Files
sap-erp/public/inventory-posting-list.html
T
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

418 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<script src="/company-list.js"></script>
<script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<title>Inventory Posting List — SAP B1</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Bebas+Neue&display=swap" rel="stylesheet"/>
<style>
:root{--ink:#080e18;--ink2:#0d1626;--purple:#9f5afd;--blue:#1a6fff;--amber:#f5a623;--green:#0dbb8a;--red:#e53e3e;--teal:#06b6d4;--border:#1c2e48;--border2:#243650;--text:#c8d6e8;--text2:#7a9ab8;--text3:#3d5470;--surface:#0f1d30;--surface2:#142338;--surface3:#0b1520;--radius:10px;--font:'Space Grotesk',sans-serif;--mono:'JetBrains Mono',monospace;--display:'Bebas Neue',sans-serif;--shadow:0 8px 32px rgba(0,0,0,.5)}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font);background:var(--ink);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
main{padding:12px;max-width:1600px;margin:0 auto}
.page-head{display:flex;align-items:center;gap:10px;margin-bottom:12px}
.page-icon{width:34px;height:34px;border-radius:9px;background:rgba(26,111,255,.14);color:var(--blue);display:flex;align-items:center;justify-content:center}
.page-title{font-size:16px;font-weight:700}
.page-sub{font-size:11px;color:var(--text3)}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden}
.card-hd{background:var(--surface3);padding:10px 14px;font-size:12px;font-weight:700;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
.card-hd .right{margin-left:auto;font-size:11px;color:var(--text3)}
.card-bd{padding:12px 14px}
.fl{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3);margin-bottom:3px;display:block}
.fi{width:100%;padding:7px 9px;border:1.5px solid var(--border2);border-radius:7px;font-size:12.5px;font-family:var(--font);background:var(--surface2);color:var(--text);outline:none;height:34px}
.fi:focus{border-color:var(--blue)}
.fi-mono{font-family:var(--mono)}
select.fi{cursor:pointer}
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
.filters>div{min-width:130px}
.range-pair{display:flex;align-items:flex-end;gap:6px}
.range-pair span{font-size:11px;color:var(--text3);padding-bottom:8px}
.btn{padding:8px 15px;border-radius:8px;border:none;font-family:var(--font);font-size:12px;font-weight:700;cursor:pointer;height:34px}
.b-primary{background:var(--blue);color:#fff}
.b-primary:disabled{opacity:.5;cursor:not-allowed}
.b-cancel{background:var(--surface2);color:var(--text2);border:1px solid var(--border2)}
.chk-row{display:flex;align-items:center;gap:7px;font-size:12px;cursor:pointer;padding:6px 0}
.wh-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:4px 14px;max-height:190px;overflow-y:auto;border:1px solid var(--border2);border-radius:8px;padding:8px 10px;background:var(--surface2)}
.wh-toolbar{display:flex;gap:10px;margin-bottom:6px;font-size:10.5px}
.wh-toolbar a{color:var(--blue);cursor:pointer;text-decoration:none}
.lst{width:100%;border-collapse:separate;border-spacing:0;font-size:12px}
.lst th{text-align:left;padding:8px 10px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3);border-bottom:2px solid var(--border2);white-space:nowrap;background:var(--surface3);position:sticky;top:0}
.lst th.sortable{cursor:pointer;user-select:none}
.lst th.sortable:hover{color:var(--blue)}
.lst td{padding:7px 10px;border-bottom:1px solid var(--border);vertical-align:top;white-space:nowrap}
.lst td.num,.lst th.num{text-align:right;font-family:var(--mono)}
.lst tr.subtotal td{background:var(--surface3);font-weight:700;color:var(--text2)}
.lst tbody tr:hover td{background:rgba(26,111,255,.05)}
.mono{font-family:var(--mono)}
.empty-msg{text-align:center;padding:26px;color:var(--text3);font-size:12px}
.count{font-size:11px;font-weight:700;color:var(--blue);background:rgba(26,111,255,.1);padding:3px 11px;border-radius:20px}
.sp{width:14px;height:14px;border:2px solid rgba(26,111,255,.25);border-top-color:var(--blue);border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.toast-c{position:fixed;top:14px;right:10px;z-index:9999;display:flex;flex-direction:column;gap:6px}
.toast{padding:9px 13px;border-radius:8px;font-size:12px;font-weight:600;box-shadow:var(--shadow)}
.t-err{background:var(--red);color:#fff}
.t-ok{background:var(--green);color:#fff}
.tbl-wrap{overflow-x:auto;max-height:65vh;overflow-y:auto;border-radius:8px;border:1px solid var(--border2)}
.search-wrap{position:relative}
.search-dd{position:absolute;top:100%;left:0;right:0;background:var(--surface2);border:1px solid var(--blue);border-radius:7px;box-shadow:0 12px 40px rgba(0,0,0,.7);z-index:300;max-height:220px;overflow-y:auto;display:none;margin-top:4px}
.sd-row{padding:10px 12px;cursor:pointer;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:center}
.sd-row:hover{background:rgba(26,111,255,.1)}
.sd-code{font-family:var(--mono);font-size:11px;color:var(--blue);font-weight:600;min-width:90px}
.sd-name{font-size:11px;color:var(--text2)}
.sd-msg{padding:12px;text-align:center;color:var(--text3);font-size:12px}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<div class="toast-c" id="toast-c"></div>
<main id="app"></main>
<script>
'use strict';
let _tok=null;const BE='/api';
try{_tok=sessionStorage.getItem('portal_token');}catch(_e){}
if(!_tok)location.href='/';
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);}
function toast(m,kind){const tc=q('#toast-c');const e=document.createElement('div');e.className='toast '+(kind==='ok'?'t-ok':'t-err');e.textContent=m;tc.appendChild(e);setTimeout(()=>e.remove(),4500);}
function num3(v){if(v==null||v==='')return'';const n=Number(v);if(isNaN(n))return'';return n.toLocaleString('en-IN',{maximumFractionDigits:6});}
function fmtD(v){if(!v)return'—';const d=new Date(v);if(isNaN(d))return'—';return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`;}
function today(){return new Date().toISOString().slice(0,10);}
async function api(p,method,body){
const headers={Authorization:'Bearer '+_tok};
const opts={method:method||'GET',headers};
if(body!=null){headers['Content-Type']='application/json';opts.body=JSON.stringify(body);}
const r=await fetch(BE+p,opts);
if(r.status===401){location.href='/';throw new Error('Session expired');}
const d=await r.json();
if(!r.ok||d.success===false)throw new Error(d.message||('HTTP '+r.status));
return d;
}
let _co=window.__DEFAULT_COMPANY__||'';
let _companies=[];
let _itemGroups=[];
let _wh=[];
let _selectedWh=new Set();
const _f={itemFrom:'',itemTo:'',itemGroup:'',dateFrom:today(),dateTo:today(),hideNoQtyChange:false,subDaily:true,subMonthly:true,subYearly:true};
let _loading=false,_ran=false,_rows=[],_search='';
let _page=0;
const PAGE_SIZE=50;
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
function grpOptions(sel){return '<option value="">All</option>'+_itemGroups.map(g=>`<option value="${esc(g.code)}" ${String(sel)===String(g.code)?'selected':''}>${esc(g.name)}</option>`).join('');}
async function loadItemGroups(){
try{const r=await api('/sap/lookup/item-groups?company='+encodeURIComponent(_co));_itemGroups=r.data||[];}
catch(_e){_itemGroups=[];}
}
async function loadWarehouses(){
try{const r=await api('/sap/lookup/warehouses?company='+encodeURIComponent(_co));_wh=r.data||[];}
catch(_e){_wh=[];}
}
async function fetchItems(v){
try{const r=await api(`/sap/lookup/items?q=${encodeURIComponent(v)}&company=${encodeURIComponent(_co)}`);if(r.success&&r.data?.length)return r.data;}catch(_e){}
return [];
}
let _codeSearchTimer=null;
function wireItemSearch(inputId,ddId,field){
const inp=q('#'+inputId),dd=q('#'+ddId);if(!inp||!dd)return;
inp.oninput=function(){
_f[field]=this.value.toUpperCase();
const v=this.value.trim();
if(v.length<2){dd.style.display='none';return;}
clearTimeout(_codeSearchTimer);
_codeSearchTimer=setTimeout(async()=>{
dd.innerHTML='<div class="sd-msg"><span class="sp"></span></div>';dd.style.display='block';
const items=await fetchItems(v);
if(_f[field]!==this.value.toUpperCase())return;
if(!items.length){dd.innerHTML='<div class="sd-msg">No items</div>';return;}
dd.innerHTML='';
items.forEach(it=>{
const row=document.createElement('div');row.className='sd-row';
row.innerHTML=`<span class="sd-code">${esc(it.ItemCode)}</span><span class="sd-name">${esc(it.ItemName)}</span>`;
row.onclick=()=>{_f[field]=it.ItemCode;inp.value=it.ItemCode;dd.style.display='none';};
dd.appendChild(row);
});
},250);
};
}
document.addEventListener('click',e=>{
if(!e.target.closest('.search-wrap'))document.querySelectorAll('.search-dd').forEach(d=>d.style.display='none');
});
async function runReport(){
if(!_f.dateFrom||!_f.dateTo){toast('Date From/To are required');return;}
if(_f.dateFrom>_f.dateTo){toast('"Date From" cannot be after "Date To"');return;}
_loading=true;_ran=true;_page=0;render();
const qs=new URLSearchParams();
qs.set('company',_co);
if(_f.itemFrom)qs.set('itemFrom',_f.itemFrom);
if(_f.itemTo)qs.set('itemTo',_f.itemTo);
if(_f.itemGroup)qs.set('itemGroup',_f.itemGroup);
qs.set('dateFrom',_f.dateFrom);qs.set('dateTo',_f.dateTo);
if(_f.hideNoQtyChange)qs.set('hideNoQtyChange','1');
if(_selectedWh.size)qs.set('warehouses',[..._selectedWh].join(','));
try{
const r=await api('/inventory-posting-list?'+qs.toString());
_rows=r.data||[];
}catch(e){toast(e.message);_rows=[];}
_loading=false;render();
}
function filteredRows(){
if(!_search.trim())return _rows;
const s=_search.trim().toUpperCase();
return _rows.filter(r=>(r.itemCode||'').toUpperCase().includes(s)||(r.itemName||'').toUpperCase().includes(s));
}
// Inserts a subtotal row (Rec./Iss. Qty sums) whenever the item OR the
// selected bucket(s) change — Daily/Monthly/Yearly toggles, matching SAP's
// own "Display Subtotal" checkboxes. Rows are already server-sorted by
// item then date, so a simple single pass is enough.
function bucketKey(row,gran){
const d=new Date(row.docDate);
if(gran==='d')return row.itemCode+'|'+d.toISOString().slice(0,10);
if(gran==='m')return row.itemCode+'|'+d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0');
return row.itemCode+'|'+d.getFullYear();
}
function buildDisplayRows(list){
const grans=[];
if(_f.subDaily)grans.push('d');
if(_f.subMonthly)grans.push('m');
if(_f.subYearly)grans.push('y');
if(!grans.length)return list.map(r=>({row:r}));
const out=[];
let prevKeys=null;
let acc=null;
const flush=()=>{
if(!acc)return;
const labels={d:'Daily Subtotal',m:'Monthly Subtotal',y:'Yearly Subtotal'};
grans.forEach(g=>{
out.push({subtotal:true,label:labels[g],inQty:acc[g].inQty,outQty:acc[g].outQty});
});
acc=null;
};
list.forEach(r=>{
const keys=grans.map(g=>bucketKey(r,g));
if(prevKeys&&keys.join('§')!==prevKeys.join('§')){flush();}
if(!acc)acc=Object.fromEntries(grans.map(g=>[g,{inQty:0,outQty:0}]));
grans.forEach(g=>{acc[g].inQty+=r.inQty;acc[g].outQty+=r.outQty;});
out.push({row:r});
prevKeys=keys;
});
flush();
return out;
}
function whToolbar(){
return `<div class="wh-toolbar"><a id="wh-all">Select all</a><a id="wh-none">Clear all</a></div>`;
}
function whGrid(){
return `<div class="wh-grid">${_wh.map(w=>`
<label class="chk-row"><input type="checkbox" class="wh-chk" value="${esc(w.code)}" ${_selectedWh.has(w.code)?'checked':''}/> <span class="mono">${esc(w.code)}</span> — ${esc(w.name||'')}</label>
`).join('')}</div>`;
}
function render(){
const app=q('#app');
const list=filteredRows();
const totalPages=Math.max(1,Math.ceil(list.length/PAGE_SIZE));
if(_page>=totalPages)_page=totalPages-1;
if(_page<0)_page=0;
const pageList=list.slice(_page*PAGE_SIZE,(_page+1)*PAGE_SIZE);
const displayRows=buildDisplayRows(pageList);
app.innerHTML=`
<div class="page-head">
<div class="page-icon">📒</div>
<div><div class="page-title">Inventory Posting List</div><div class="page-sub">Every stock-moving transaction per item, with a running Balance — same as SAP B1's own Inventory Posting List report</div></div>
</div>
<div class="card">
<div class="card-hd"><span>Selection Criteria</span></div>
<div class="card-bd">
<div class="filters" style="margin-bottom:10px">
<div style="min-width:180px"><label class="fl">Company</label><select class="fi" id="sel-co">${coOptions(_co)}</select></div>
<div><label class="fl">Item Code</label><div class="range-pair">
<div class="search-wrap" style="flex:1"><input class="fi fi-mono" id="f-itemfrom" placeholder="Search item code / name…" value="${esc(_f.itemFrom)}" autocomplete="off"/><div class="search-dd" id="dd-itemfrom"></div></div>
<span>to</span>
<div class="search-wrap" style="flex:1"><input class="fi fi-mono" id="f-itemto" placeholder="Search item code / name…" value="${esc(_f.itemTo)}" autocomplete="off"/><div class="search-dd" id="dd-itemto"></div></div>
</div></div>
<div><label class="fl">Item Group</label><select class="fi" id="f-itemgroup">${grpOptions(_f.itemGroup)}</select></div>
<div><label class="fl">Date</label><div class="range-pair"><input class="fi" id="f-datefrom" type="date" value="${esc(_f.dateFrom)}"/><span>to</span><input class="fi" id="f-dateto" type="date" value="${esc(_f.dateTo)}"/></div></div>
<div><button class="btn b-primary" id="btn-run" ${_loading?'disabled':''}>${_loading?'Running…':'Run Report'}</button></div>
</div>
<label class="chk-row"><input type="checkbox" id="f-hidenoqty" ${_f.hideNoQtyChange?'checked':''}/> Hide Trans. without Qty Change</label>
<div style="display:flex;gap:16px;margin-top:4px">
<span class="fl" style="margin:0;align-self:center">Display Subtotal:</span>
<label class="chk-row" style="padding:4px 0"><input type="checkbox" id="f-subdaily" ${_f.subDaily?'checked':''}/> Daily</label>
<label class="chk-row" style="padding:4px 0"><input type="checkbox" id="f-submonthly" ${_f.subMonthly?'checked':''}/> Monthly</label>
<label class="chk-row" style="padding:4px 0"><input type="checkbox" id="f-subyearly" ${_f.subYearly?'checked':''}/> Yearly</label>
</div>
<div style="margin-top:10px">
<label class="fl">Warehouses <span style="font-weight:400;text-transform:none;color:var(--text3)">(none selected = all warehouses)</span></label>
${whToolbar()}
${whGrid()}
</div>
</div>
</div>
<div class="card">
<div class="card-hd"><span>Results</span><span class="right">${_ran&&!_loading?`<span class="count">${list.length} transaction(s)</span>`:''}</span></div>
<div class="card-bd">
<div style="margin-bottom:10px;display:flex;gap:8px;align-items:center">
<input class="fi" id="f-search" placeholder="Item No. / Description…" value="${esc(_search)}" style="max-width:320px"/>
${_ran&&list.length?`<button class="btn b-cancel" id="btn-export" style="margin-left:auto">⬇ Export Excel</button>`:''}
</div>
${_loading?`<div class="empty-msg"><span class="sp"></span> Running report…</div>`
:!_ran?`<div class="empty-msg">Set your filters above and click "Run Report".</div>`
:!list.length?`<div class="empty-msg">No transactions found for these filters.</div>`
:`<div class="tbl-wrap"><table class="lst"><thead><tr>
<th class="num">#</th><th>Posting Date</th><th>Item No.</th><th>Item Description</th><th>Document</th><th>Whse</th>
<th class="num">Doc.Row</th><th>BP Name</th><th class="num">Rec. Qty</th><th class="num">Iss. Qty</th>
<th class="num">Price</th><th class="num">Balance</th><th>Remarks</th><th>Ref. 2</th>
</tr></thead><tbody>${displayRows.map((d,i)=>{
if(d.subtotal)return `<tr class="subtotal"><td colspan="8">${esc(d.label)}</td><td class="num">${num3(d.inQty)}</td><td class="num">${num3(d.outQty)}</td><td colspan="4"></td></tr>`;
const r=d.row;
return `<tr>
<td class="num">${_page*PAGE_SIZE+i+1}</td>
<td>${fmtD(r.docDate)}</td>
<td class="mono">${esc(r.itemCode)}</td>
<td>${esc(r.itemName)}</td>
<td>${esc(r.docLabel)}${r.docRef?' '+esc(r.docRef):''}</td>
<td>${esc(r.warehouse)}</td>
<td class="num">${r.docRow}</td>
<td>${esc(r.bpName)}</td>
<td class="num">${r.inQty?num3(r.inQty):''}</td>
<td class="num">${r.outQty?num3(r.outQty):''}</td>
<td class="num">${num3(r.price)}</td>
<td class="num">${num3(r.balance)}</td>
<td>${esc(r.remarks)}</td>
<td>${esc(r.ref2)}</td>
</tr>`;
}).join('')}</tbody></table></div>
<div style="display:flex;align-items:center;gap:10px;margin-top:10px;justify-content:flex-end">
<span style="font-size:11px;color:var(--text3)">Page ${_page+1} of ${totalPages}</span>
<button class="btn b-cancel" id="btn-prev" ${_page===0?'disabled':''} style="padding:6px 14px;height:auto">‹ Prev</button>
<button class="btn b-cancel" id="btn-next" ${_page>=totalPages-1?'disabled':''} style="padding:6px 14px;height:auto">Next ›</button>
</div>`}
</div>
</div>`;
wire();
}
function wire(){
q('#sel-co').onchange=async function(){_co=this.value;_ran=false;_rows=[];await Promise.all([loadItemGroups(),loadWarehouses()]);_selectedWh=new Set();render();};
wireItemSearch('f-itemfrom','dd-itemfrom','itemFrom');
wireItemSearch('f-itemto','dd-itemto','itemTo');
q('#f-itemgroup').onchange=function(){_f.itemGroup=this.value;};
q('#f-datefrom').onchange=function(){_f.dateFrom=this.value;};
q('#f-dateto').onchange=function(){_f.dateTo=this.value;};
q('#f-hidenoqty').onchange=function(){_f.hideNoQtyChange=this.checked;};
q('#f-subdaily').onchange=function(){_f.subDaily=this.checked;if(_ran)render();};
q('#f-submonthly').onchange=function(){_f.subMonthly=this.checked;if(_ran)render();};
q('#f-subyearly').onchange=function(){_f.subYearly=this.checked;if(_ran)render();};
q('#btn-run').onclick=runReport;
q('#f-search').oninput=function(){
_search=this.value;_page=0;
const pos=this.selectionStart;
render();
const el=q('#f-search');if(el){el.focus();try{el.setSelectionRange(pos,pos);}catch(_e){}}
};
if(q('#btn-prev'))q('#btn-prev').onclick=()=>{_page--;render();};
if(q('#btn-next'))q('#btn-next').onclick=()=>{_page++;render();};
document.querySelectorAll('.wh-chk').forEach(el=>{
el.onchange=function(){
if(this.checked)_selectedWh.add(this.value);else _selectedWh.delete(this.value);
};
});
q('#wh-all').onclick=()=>{_selectedWh=new Set(_wh.map(w=>w.code));render();};
q('#wh-none').onclick=()=>{_selectedWh=new Set();render();};
if(q('#btn-export'))q('#btn-export').onclick=exportExcel;
}
// Styled .xlsx export via ExcelJS — same look/border convention as the other
// reports in this app (bordered header banner + criteria summary, bordered/
// zebra-striped data grid, frozen header row). Exports exactly what's
// currently filtered on screen (search box), not just the current page.
const XLSX_BLUE='FF1A6FFF', XLSX_INK='FF1E2A44', XLSX_WHITE='FFFFFFFF', XLSX_ZEBRA='FFEFF4FF';
function xlsxBorder(color){ const b={style:'thin',color:{argb:color||'FFB9CCEA'}}; return {top:b,left:b,bottom:b,right:b}; }
async function exportExcel(){
const rows=filteredRows();
if(!rows.length){toast('Nothing to export');return;}
if(typeof ExcelJS==='undefined'){toast('Export library failed to load — check your connection and retry');return;}
const wb=new ExcelJS.Workbook();
wb.creator='SAP ERP Portal';wb.created=new Date();
const ws=wb.addWorksheet('Inventory Posting List',{views:[{state:'frozen',ySplit:6}]});
ws.mergeCells('A1:N1');
const title=ws.getCell('A1');
title.value='Inventory Posting List — Export';
title.font={bold:true,size:16,color:{argb:XLSX_WHITE}};
title.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_BLUE}};
title.alignment={vertical:'middle',horizontal:'left'};
ws.getRow(1).height=26;
const whLabel=_selectedWh.size?[..._selectedWh].join(', '):'All';
const metaRows=[
['Company:',_co,'Date:',`${_f.dateFrom} to ${_f.dateTo}`],
['Warehouse(s):',whLabel,'Rows:',rows.length],
];
metaRows.forEach((r,i)=>{
const row=ws.getRow(2+i);
row.getCell(1).value=r[0];row.getCell(1).font={bold:true,color:{argb:'FF64748B'}};
row.getCell(2).value=r[1];row.getCell(2).font={color:{argb:'FF0F172A'}};
row.getCell(3).value=r[2];row.getCell(3).font={bold:true,color:{argb:'FF64748B'}};
row.getCell(4).value=r[3];row.getCell(4).font={color:{argb:'FF0F172A'}};
});
const hdrRowNum=6;
const cols=['#','Posting Date','Item No.','Item Description','Document','Whse','Doc.Row','BP Name','Rec. Qty','Iss. Qty','Price','Balance','Remarks','Ref. 2'];
const hdrRow=ws.getRow(hdrRowNum);
cols.forEach((h,i)=>{
const cell=hdrRow.getCell(i+1);
cell.value=h;
cell.font={bold:true,color:{argb:XLSX_WHITE}};
cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:XLSX_INK}};
cell.alignment={vertical:'middle',horizontal:(i===0||(i>=8&&i<=11))?'right':'center'};
cell.border=xlsxBorder('FF0F172A');
});
hdrRow.height=20;
rows.forEach((r,i)=>{
const row=ws.getRow(hdrRowNum+1+i);
const vals=[i+1,fmtD(r.docDate),r.itemCode,r.itemName,`${r.docLabel}${r.docRef?' '+r.docRef:''}`,r.warehouse,r.docRow,r.bpName,r.inQty||'',r.outQty||'',r.price,r.balance,r.remarks,r.ref2];
vals.forEach((v,ci)=>{
const cell=row.getCell(ci+1);
cell.value=v;
cell.border=xlsxBorder();
cell.fill={type:'pattern',pattern:'solid',fgColor:{argb:i%2?XLSX_ZEBRA:XLSX_WHITE}};
if(ci===0||(ci>=8&&ci<=11)){cell.numFmt=ci===0?'0':'#,##0.######';cell.alignment={horizontal:'right'};}
});
});
ws.columns=[{width:6},{width:12},{width:16},{width:30},{width:20},{width:8},{width:9},{width:22},{width:12},{width:12},{width:12},{width:12},{width:16},{width:14}];
const buf=await wb.xlsx.writeBuffer();
const blob=new Blob([buf],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download=`Inventory_Posting_List_${_f.dateFrom}_to_${_f.dateTo}.xlsx`;
a.click();
URL.revokeObjectURL(a.href);
}
async function init(){
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
await Promise.all([loadItemGroups(),loadWarehouses()]);
render();
}
init();
</script>
<script src="/sidebar.js?v=31"></script>
</body>
</html>