BOM Control

DEMO
`); w.document.close(); return w; } function getProj(projId){ return projId?DB.projects.find(p=>p.id===projId):null; } function projLabel(projId){ const p=getProj(projId); return p?`${p.code} — ${p.name}`:'Todos los proyectos'; } function badgePDF(s){ const map={pendiente:'b-yellow',entregado:'b-green',parcial:'b-blue',cancelado:'b-red',activo:'b-green',aprobado:'b-green',borrador:'b-gray',anulado:'b-red'}; return `${s}`; } function generatePDF(key, projId){ const pid=projId||''; switch(key){ case 'solicitudes': return pdfSolicitudes(pid); case 'entregas': return pdfEntregas(pid); case 'faltantes': return pdfFaltantes(pid); case 'bom': return pdfBOM(pid); case 'presupuesto': return pdfPresupuesto(pid); case 'final': return pdfFinalBOM(pid); } } function pdfSolicitudes(projId){ const reqs=DB.requests.filter(r=>!projId||r.project_id===projId); const byStatus={pendiente:0,entregado:0,parcial:0,cancelado:0}; reqs.forEach(r=>{byStatus[r.status]=(byStatus[r.status]||0)+1;}); const rows=reqs.map(r=>{ const p=DB.projects.find(x=>x.id===r.project_id); return ` ${r.code} ${p?p.code:'—'} ${r.user} ${r.date||'—'} ${r.items?r.items.length:0} ítem(s) ${badgePDF(r.status)} `;}).join(''); const kpis=`
Total
${reqs.length}
Pendientes
${byStatus.pendiente||0}
Entregados
${byStatus.entregado||0}
Parciales
${byStatus.parcial||0}
Cancelados
${byStatus.cancelado||0}
`; const body=`${kpis}${rows||''}
CódigoProyectoSolicitanteFechaÍtemsEstado
Sin registros
`; pdfBase('Reporte de Solicitudes', projLabel(projId), body); } function pdfEntregas(projId){ const devs=DB.deliveries.filter(d=>!projId||(()=>{const r=DB.requests.find(x=>x.id===d.req_id);return r&&r.project_id===projId;})()); const rows=devs.map(d=>{ const req=DB.requests.find(r=>r.id===d.req_id); const proj=req?DB.projects.find(p=>p.id===req.project_id):null; const totalItems=(d.items||[]).reduce((s,i)=>s+(i.qty_del||0),0); return ` ${d.code||d.id} ${proj?proj.code:'—'} ${req?req.code:'—'} ${d.date||'—'} ${totalItems} ${d.receiver||'—'} `;}).join(''); const kpis=`
Total entregas
${devs.length}
Ítems totales
${devs.reduce((s,d)=>s+(d.items||[]).reduce((a,i)=>a+(i.qty_del||0),0),0)}
`; const body=`${kpis}${rows||''}
EntregaProyectoSolicitudFechaCant. totalReceptor
Sin entregas
`; pdfBase('Entregas Totales', projLabel(projId), body); } function pdfFaltantes(projId){ const pendReqs=DB.requests.filter(r=>(!projId||r.project_id===projId)&&r.status==='pendiente'); const rows=pendReqs.map(r=>{ const p=DB.projects.find(x=>x.id===r.project_id); const items=(r.items||[]).map(it=>`
• ${it.code||''} ${it.name||''} — ${it.qty||0} ${it.unit||'und'}
`).join(''); return ` ${r.code} ${p?p.code:'—'} ${r.user} ${r.date||'—'} ${(r.items||[]).length} ${r.priority||'normal'} ${items?`${items}`:''}`;}).join(''); const bomFaltantes=DB.bom.filter(b=>(!projId||b.project_id===projId)&&b.del{ const p=DB.projects.find(x=>x.id===b.project_id); const falta=b.qty-b.del; return ` ${b.code} ${p?p.code:'—'} ${b.name} ${b.qty} ${b.del} ${falta} ${b.unit} `;}).join(''); const body=`
Solicitudes pendientes
${pendReqs.length}
Ítems BOM faltantes
${bomFaltantes.length}
Solicitudes pendientes
${rows||''}
CódigoProyectoSolicitanteFechaÍtemsPrioridad
Sin solicitudes pendientes
Materiales BOM con entrega incompleta
${bomRows||''}
CódigoProyectoDescripciónRequeridoEntregadoFaltanteUnidad
Sin faltantes
`; pdfBase('Reporte de Faltantes', projLabel(projId), body); } function pdfBOM(projId){ const items=DB.bom.filter(b=>!projId||b.project_id===projId); const rows=items.map(b=>{ const p=DB.projects.find(x=>x.id===b.project_id); const pp=b.qty>0?Math.round((b.del/b.qty)*100):0; const barColor=pp>=100?'#FF3B30':pp>=80?'#FF9500':'#34C759'; return ` ${b.code} ${p?p.code:'—'} ${b.name} ${b.qty} ${b.del} ${b.qty-b.del>0?b.qty-b.del:0} ${b.unit} ${pp}% `;}).join(''); const total=items.length; const complet=items.filter(b=>b.del>=b.qty).length; const body=`
Total ítems
${total}
Completados
${complet}
Pendientes
${total-complet}
Avance global
${total>0?Math.round((complet/total)*100):0}%
${rows||''}
CódigoProyectoDescripciónReq.EntregadoFaltanteUnidad%
Sin ítems
TOTAL${items.reduce((s,b)=>s+b.qty,0)}${items.reduce((s,b)=>s+b.del,0)}${items.reduce((s,b)=>s+Math.max(0,b.qty-b.del),0)}
`; pdfBase('BOM Completo', projLabel(projId), body); } function pdfPresupuesto(projId){ const projs=projId?DB.projects.filter(p=>p.id===projId):DB.projects; const rows=projs.map(p=>{ const bomItems=DB.bom.filter(b=>b.project_id===p.id); const costReal=bomItems.reduce((s,b)=>s+(b.real_price||b.price||0)*b.del,0); const laborTotal=DB.labor.filter(l=>l.project_id===p.id).reduce((s,l)=>s+l.cost,0); const viatTotal=DB.viaticos.filter(v=>v.project_id===p.id).reduce((s,v)=>s+v.monto,0); const total=costReal+laborTotal+viatTotal; const budget=p.budget||0; const saldo=budget-total; return ` ${p.code} ${p.name} ₡${fmtN(budget)} ₡${fmtN(costReal)} ₡${fmtN(laborTotal)} ₡${fmtN(viatTotal)} ₡${fmtN(total)} ₡${fmtN(saldo)} `;}).join(''); const totBudget=projs.reduce((s,p)=>s+(p.budget||0),0); const totMat=projs.reduce((s,p)=>{const b=DB.bom.filter(x=>x.project_id===p.id);return s+b.reduce((a,x)=>a+(x.real_price||x.price||0)*x.del,0);},0); const totLab=projs.reduce((s,p)=>s+DB.labor.filter(l=>l.project_id===p.id).reduce((a,l)=>a+l.cost,0),0); const totViat=projs.reduce((s,p)=>s+DB.viaticos.filter(v=>v.project_id===p.id).reduce((a,v)=>a+v.monto,0),0); const totReal=totMat+totLab+totViat; const body=`
Presupuesto total
₡${fmtN(totBudget)}
Costo materiales
₡${fmtN(totMat)}
Mano de obra
₡${fmtN(totLab)}
Viáticos
₡${fmtN(totViat)}
Costo total real
₡${fmtN(totReal)}
${rows}
CódigoProyectoPresupuestoMaterialesM.O.ViáticosTotal realSaldo
TOTALES₡${fmtN(totBudget)}₡${fmtN(totMat)}₡${fmtN(totLab)}₡${fmtN(totViat)}₡${fmtN(totReal)}₡${fmtN(totBudget-totReal)}
`; pdfBase('Presupuesto — Costos y Saldos', projLabel(projId), body); } function pdfFinalBOM(projId){ const items=DB.bom.filter(b=>!projId||b.project_id===projId); const rows=items.map(b=>{ const p=DB.projects.find(x=>x.id===b.project_id); // Sum all returns for this BOM item const devTotal=b.del; const retItems=DB.returns.flatMap(r=>r.items||[]).filter(i=>i.bom_id===b.id); const retTotal=retItems.reduce((s,i)=>s+(i.qty_ret||0),0); const neto=devTotal-retTotal; const unitCost=b.real_price||b.price||0; return ` ${b.code} ${p?p.code:'—'} ${b.name} ${b.unit} ${b.qty} ${devTotal} ${retTotal} ${neto} ₡${fmtN(unitCost)} ₡${fmtN(unitCost*neto)} `;}).join(''); const totDev=items.reduce((s,b)=>s+b.del,0); const totRet=items.reduce((s,b)=>{const ri=DB.returns.flatMap(r=>r.items||[]).filter(i=>i.bom_id===b.id);return s+ri.reduce((a,i)=>a+(i.qty_ret||0),0);},0); const totNeto=totDev-totRet; const totCost=items.reduce((s,b)=>{const ri=DB.returns.flatMap(r=>r.items||[]).filter(i=>i.bom_id===b.id);const ret=ri.reduce((a,i)=>a+(i.qty_ret||0),0);return s+(b.real_price||b.price||0)*(b.del-ret);},0); const body=`
Total entregado
${totDev}
Total devuelto
${totRet}
Consumo neto
${totNeto}
Costo neto total
₡${fmtN(totCost)}
${rows||''}
CódigoProyectoDescripciónUnidadRequeridoEntregadoDevueltoNetoP. Unit.Costo neto
Sin ítems
TOTALES${items.reduce((s,b)=>s+b.qty,0)}${totDev}${totRet}${totNeto}₡${fmtN(totCost)}
`; pdfBase('Reporte Final BOM — Consumo Neto', projLabel(projId), body); } function renderResumenTab(proj){ const items=DB.bom.filter(b=>b.project_id===proj.id); const totalDel=items.reduce((s,i)=>s+i.del,0); const totalQty=items.reduce((s,i)=>s+i.qty,0); const pp=pct(totalDel,totalQty); const costBOM=items.reduce((s,i)=>s+i.qty*i.price,0); const costReal=items.reduce((s,i)=>s+i.del*(i.real_price||i.price),0); const laborTotal=DB.labor.filter(l=>l.project_id===proj.id).reduce((s,l)=>s+l.cost,0); const viaticosTotal=DB.viaticos.filter(v=>v.project_id===proj.id).reduce((s,v)=>s+v.monto,0); const costoTotalReal=costReal+laborTotal+viaticosTotal; return `
${donutChart(pp,80)}
Avance global
${fmtN(totalDel)} / ${fmtN(totalQty)} unidades
Costo BOM planificado
${fmt(costBOM)}
Costo real materiales
${fmt(costReal)}
Mano de obra
${fmt(laborTotal)}
Ppto M.O.: ${proj.labor_budget?fmt(proj.labor_budget):'—'}
Viáticos
${fmt(viaticosTotal)}
Ppto: ${proj.viaticos_budget?fmt(proj.viaticos_budget):'—'}
Costo total real (materiales + M.O. + viáticos)
${fmt(costoTotalReal)}
`; } function vBOM(projectId){ const pid=projectId||P1; const proj=DB.projects.find(p=>p.id===pid)||DB.projects[0]; const items=DB.bom.filter(b=>b.project_id===proj.id); const cats=[...new Set(items.map(i=>i.cat))]; return `
${cats.map(c=>``).join('')}
${items.map(i=>{ const st=bomStats(i.id,i.qty); const consumed=st.net; const p=pct(consumed,i.qty); const realCost=consumed*(i.real_price||i.price||0); const dispColor=st.available<0?'var(--c-red)':st.available===0?'var(--c-text-muted)':'var(--c-green)'; return ``;}).join('')}
CódigoProductoCategoríaUn.Total BOMSolicitadoEntregadoConsumidoReservadoDisponibleP.Unit RealCosto Real
${i.code} ${i.name} ${i.cat} ${i.unit} ${fmtN(i.qty)} ${fmtN(i.req)} ${fmtN(i.del)}
${fmtN(consumed)}
${fmtN(st.reserved)} ${fmtN(st.available)}${st.available<0?' ⚠':''} ${fmt(i.real_price||i.price)} ${fmt(realCost)}
`; } function vBOMUpload(projectId){ const pid=projectId||P1; const proj=DB.projects.find(p=>p.id===pid)||DB.projects[0]; return `
Importar BOM desde Excel
Proyecto: ${proj.name}
📊
Haga clic para seleccionar
.xlsx o .xls — máximo 10 MB
Formato requerido
ColumnaRequeridaEjemplo
codigoSíELE-001
nombre_productoSíCable THHN #10
descripcionNoCable cobre THHN
unidadSím, und, kg, lám
cantidad_totalSí1500
categoriaNoEléctrico
precio_unitarioNo450.00
`; } function vRequests(filterProjId){ const statuses=['','pendiente','entregado','parcial','cancelado']; let reqs=DB.requests; let activeFilter=''; return `
${['pendiente','entregado','parcial','cancelado'].map(s=>``).join('')}
${canDo('create-request')?'':''}
${DB.requests.map(r=>{ const proj=DB.projects.find(p=>p.id===r.project_id); const items=DB.req_items.filter(i=>i.req_id===r.id); return ``;}).join('')}
ID SolicitudProyectoCreada porFechaÍtemsEstadoAcciones
${r.req_id} ${proj?proj.code:'—'} ${r.user} ${r.created} ${items.length} ${statusBadge(r.status)} ${r.status==='pendiente'?``:''}
`; } function vNewRequest(){ const bomByProj={}; for(const p of DB.projects) bomByProj[p.id]=DB.bom.filter(b=>b.project_id===p.id); return `
Nueva solicitud de materiales
Haga clic en "+ Agregar ítem" para comenzar