TIC911 · Inventario Digital
Suite de herramientas · Control profesional de stock
Inventario TIC911 · Profesional
Catálogos editables · Movimientos por ticket y CSV · Listo para exportar
Módulo Inventario

Inventario TIC911 · Profesional

Flujo recomendado: 1) Descarga la plantilla · 2) Llena tu catálogo · 3) Carga el inventario
Paso 1 · Plantilla
Paso 2 · Catálogo
Paso 3 · Carga

Catálogo base de inventario

Usa la plantilla TIC911 para crear o actualizar tu catálogo de productos. Luego cárgalo para trabajar con filtros, movimientos y tickets.
Incluye columnas: Código, Nombre, Unidad, Stock, Mín/Máx, Categoría, Proveedor, Ubicación, Precio.
Archivo de inventario
Sin archivos seleccionados
Recomendado: CSV delimitado por comas para máxima compatibilidad en catálogo y movimientos.
Busca y filtra tu inventario actual.
📘 Historial avanzado
Herramientas: Renombrar categoría/proveedor · Nuevo producto · Cargar movimientos CSV · Nueva categoría · Nuevo proveedor
Campos soportados: Código Nombre Unidad Stock Mín Máx Categoría Proveedor Ubicación Precio Acciones
Código Producto Unidad Stock Sist Mín Máx Categoría Proveedor Ubicación Precio Acciones
Movimientos individuales (por código) Escanea o escribe el código, indica cantidad y elige Entrada/Salida.
Atajos: ENTER en cantidad envía, ESC limpia, el foco vuelve al campo de código.
Cargar movimientos (CSV) Descargar plantilla Columnas: code,type,qty,note (o equivalentes).
Entradas por Ticket (foto/PDF)
Arrastra la foto o PDF del reporte/ticket o haz clic para seleccionar. El sistema hará OCR del Código y la Cantidad para pre-cargar la entrada.
Suelta aquí una imagen (.jpg/.png) o PDF, o haz clic para elegir archivo.
Tamaños recomendados: nítido, bien iluminado; PDF renderizado a 200-300 DPI funciona mejor.
'; const blob = new Blob([html], {type:'application/vnd.ms-excel'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='inventario_'+(new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'))+'.xlsx'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); return {ok:true, name:a.download}; } function exportPDF(rows){ // Print-based PDF: open a minimal printable view and call print() const header = ['Código','Producto','Unidad','Stock','Sist','Mín','Máx','Categoría','Proveedor','Ubicación','Precio']; const trh = ''+header.map(h=>''+h+'').join('\n')+''; const trs = rows.map(r=>''+[r.code,r.name,r.unit,r.stock,r.sist,r.min,r.max,r.category,r.supplier,r.location,r.price].map(v=>''+String(v).replace(/[<>]/g,'')+'').join('\n')+'').join('\n'); const w = window.open('', '_blank'); if(!w){ alert('Desbloquea popups para generar PDF'); return {ok:false}; } w.document.write('Inventario

Inventario

'+trh+trs+'
'); w.document.close(); w.focus(); w.print(); return {ok:true, name:'inventario.pdf'}; } async function serverSaveSnapshotFiles(snapId, fmts){ try{ const done = []; for(const fmt of fmts){ const u = '/api/inventory/snapshot/'+encodeURIComponent(snapId)+'/export?fmt='+encodeURIComponent(fmt); const r = await fetch(u, { credentials:'include' }); if(r.ok){ done.push(fmt); } } if(done.length) toast('Archivos guardados en el servidor: '+done.join(', '), true); return {ok:true, fmts:fmts}; }catch(e){ return {ok:false}; } } function addLocalHistItem(item){ try{ const k='tic911_inv_files'; const arr = JSON.parse(localStorage.getItem(k)||'[]'); arr.unshift(item); localStorage.setItem(k, JSON.stringify(arr.slice(0,100))); }catch(_){} } async function loadHistorial(){ const box = document.getElementById('historialList'); if(!box) return; box.innerHTML = '
Cargando...
'; // server-first try{ const r = await fetch('/api/inventory/snapshots', { credentials:'include' }); if(r.ok){ const j = await r.json(); if(j && Array.isArray(j.items)){ box.innerHTML = j.items.map(s => '
'+ (s.name||('snapshot '+s.id)) +' · '+ (s.mode||'') +' · '+ (s.status||'') + ' · CSV · Excel · PDF
').join('\n'); return; } } }catch(_){} // fallback local try{ const k='tic911_inv_files'; const arr = JSON.parse(localStorage.getItem(k)||'[]'); if(arr.length){ box.innerHTML = arr.map(x => '
'+x.name+' · '+x.when+' · Descargar
').join('\n'); }else{ box.innerHTML = '
Sin registros.
'; } }catch(_){ box.innerHTML = '
Sin registros.
'; } } // === Integrate "Guardar actual" and "Nuevo en 0" with server save === async function saveCurrentInventoryToServer(){ // Prefer server snapshot if available try{ const r = await fetch('/api/inventory/snapshot', { method:'POST', credentials:'include', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ mode:'current', name:'Inventario '+(new Date().toLocaleString()) }) }); if(r.ok){ const j = await r.json(); if(j && j.snapshot_id){ await serverSaveSnapshotFiles(j.snapshot_id, ['csv','xls','pdf']); toast('Inventario guardado como snapshot #'+j.snapshot_id, true); return {ok:true}; } } }catch(_){} // Fallback: client export CSV and record locally const res = exportCSV(tableDataForExport()); if(res.ok) toast('Inventario guardado localmente (CSV).', true); return {ok:true, local:true}; } async function createZeroInventory(){ // set Sist=0 visually startCountFromStock(0); // Try server-side zero snapshot try{ const r = await fetch('/api/inventory/snapshot', { method:'POST', credentials:'include', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ mode:'zero', name:'Inventario en 0 '+(new Date().toLocaleString()) }) }); if(r.ok){ const j = await r.json(); if(j && j.snapshot_id){ toast('Snapshot en 0 creado (#'+j.snapshot_id+').', true); return {ok:true}; } } }catch(_){} toast('Conteo en 0 preparado (local).', true); return {ok:true, local:true}; } // Hook "Guardar actual" and "Nuevo en 0" buttons if present document.addEventListener('DOMContentLoaded', ()=>{ const bSave = document.getElementById('btnSaveSnapshot'); const bZero = document.getElementById('btnNewZero'); if(bSave){ bSave.addEventListener('click', saveCurrentInventoryToServer); } if(bZero){ bZero.addEventListener('click', createZeroInventory); } }); // === Fallback: subir CSV con snapshot automático === async function uploadCsvWithSnapshot(label){ // Generar CSV const headers = ['Código','Producto','Unidad','Stock','Mín','Máx','Categoría','Proveedor','Ubicación','Precio']; const esc = (s)=>{ s = (s==null?'':String(s)); if (/[",;\n]/.test(s)) return '"' + s.replace(/"/g,'""') + '"'; return s; }; let csv = headers.join(',') + '\n'; for(const p of state.items){ const row = [ esc(p.code||''), esc(p.name||''), esc(p.unit||''), esc(Number(p.stock||0)), esc(Number(p.stock_min||0)), esc(Number(p.stock_max||0)), esc(p.category||''), esc(p.supplier||''), esc(p.location||''), esc(Number(p.price||0)) ]; csv += row.join(',') + '\n'; } // Enviar multipart/form-data const blob = new Blob([csv], {type:'text/csv'}); const fd = new FormData(); fd.append('file', blob, 'inventario.csv'); fd.append('snapshot', '1'); fd.append('also_snapshot', '1'); fd.append('title', label); const url = `/api/inventory/upload?snapshot=1&also_snapshot=1&title=${encodeURIComponent(label)}`; const resp = await fetch(url, { method:'POST', body: fd, credentials:'include' }); if(!resp.ok){ const t = await resp.text().catch(()=>''); throw new Error(`Fallback upload failed (${resp.status}): ` + t); } const j = await resp.json().catch(()=>({})); return j; } // === Guardar & Historial (Opción B) === async function saveAndSnapshot(){ try{ const now = new Date(); const pad = (n)=> (n<10?('0'+n):n); const defLabel = `Inventario ${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`; const label = prompt('Título del historial:', defLabel) || defLabel; const payload = { label, items: tableDataForServer() }; // Intento 1: endpoint Opción B (JSON) let resp = await fetch('/api/inventory/save_and_snapshot', { method: 'POST', headers: {'Content-Type':'application/json'}, credentials: 'include', body: JSON.stringify(payload) }); if(resp.status === 405){ // Fallback: subir CSV a /upload con snapshot=1 const j2 = await uploadCsvWithSnapshot(label); toast('Guardado & Historial (fallback CSV) listo', true); try{ await loadHistorial(); }catch(_){} return; } if(!resp.ok){ const t = await resp.text().catch(()=>''); throw new Error(`save_and_snapshot HTTP ${resp.status}: ` + t); } const j = await resp.json().catch(()=>({})); toast('Guardado y enviado al historial (#'+(j.snapshot_id||'?')+')', true); try{ await loadHistorial(); }catch(_){} }catch(e){ console.error('saveAndSnapshot error', e); toast('No se pudo guardar al historial: '+(e.message||e), false); } } document.addEventListener('DOMContentLoaded', ()=>{ const b = document.getElementById('btnSaveAndHist'); if(b){ b.addEventListener('click', saveAndSnapshot); } }); // === Historial helpers (listar, abrir, pintar, clonar) === function normalizeSnapshotItems(items){ return (items||[]).map(x => ({ id: x.id, code: x.code||x.product_code||x.codigo||x.codigo_barras, name: x.name||x.nombre, unit: x.unit||x.unidad, stock: Number(x.stock||0), stock_min: Number(x.stock_min||x.min||0), stock_max: Number(x.stock_max||x.max||0), category: x.category||x.categoria, supplier: x.supplier||x.proveedor, location: x.location||x.ubicacion, price: Number(x.price||x.unit_price||0), __sist: Number(x.__sist||x.sist||x.stock||0) })); } async function viewSnapshot(id,label){ const pv = document.getElementById('historialPreview'); if(!pv) return; pv.innerHTML = '
Cargando…
'; try{ const r = await fetch(`/api/inventory/snapshot/${encodeURIComponent(id)}`, {credentials:'include'}); const j = await r.json().catch(()=>({})); if(!r.ok || j.ok===false) throw new Error(j.error||('HTTP '+r.status)); const items = normalizeSnapshotItems(j.items||[]); if(!items.length){ pv.innerHTML = '
Snapshot vacío.
'; return; } const head = ['Código','Producto','Unidad','Stock','Mín','Máx','Categoría','Proveedor','Ubicación','Precio']; const rows = items.map(p=>` ${p.code||''}${p.name||''}${p.unit||''} ${p.stock||0}${p.stock_min||0}${p.stock_max||0} ${p.category||''}${p.supplier||''}${p.location||''} ${p.price||0}`).join(''); pv.innerHTML = `
Vista previa — ${label||('Snapshot #'+id)}
${head.map(h=>``).join('')}${rows}
${h}
`; }catch(e){ pv.innerHTML = '
No se pudo abrir este snapshot.
'; } } async function paintSnapshot(id,label){ try{ const r = await fetch(`/api/inventory/snapshot/${encodeURIComponent(id)}`, {credentials:'include'}); const j = await r.json().catch(()=>({})); if(!r.ok || j.ok===false) throw new Error(j.error||('HTTP '+r.status)); const items = normalizeSnapshotItems(j.items||[]); if(!window.state.view) window.state.view = {mode:'current', display:[], snapshot:null}; state.view.mode = 'history'; state.view.display = items; state.view.snapshot = {id, label: label||('Snapshot #'+id)}; const banner = document.getElementById('histViewBanner'); const lbl = document.getElementById('histViewLabel'); if(banner){ banner.style.display='block'; if(lbl) lbl.textContent = state.view.snapshot.label; } if(typeof render==='function') render(); toast('Mostrando historial en la tabla (solo lectura)', true); }catch(e){ console.error('paintSnapshot', e); toast('No se pudo pintar el historial: '+(e.message||e), false); } } async function cloneSnapshotToEditable(id){ try{ const r = await fetch(`/api/inventory/snapshot/${encodeURIComponent(id)}`, {credentials:'include'}); const j = await r.json().catch(()=>({})); if(!r.ok || j.ok===false) throw new Error(j.error||('HTTP '+r.status)); const items = normalizeSnapshotItems(j.items||[]); if(state.view){ state.view.mode='current'; state.view.display=[]; } state.items = items.map(p => { p.__sist = Number(p.__sist||p.stock||0); return p; }); if(typeof render==='function') render(); const banner = document.getElementById('histViewBanner'); if(banner) banner.style.display='none'; toast('Snapshot clonado a modo editable', true); }catch(e){ console.error('cloneSnapshotToEditable', e); toast('No se pudo clonar: '+(e.message||e), false); } } document.addEventListener('DOMContentLoaded', ()=>{ const exit = document.getElementById('histViewExit'); if(exit){ exit.addEventListener('click', ()=>{ if(state.view){ state.view.mode='current'; state.view.display=[]; state.view.snapshot=null; } const banner = document.getElementById('histViewBanner'); if(banner) banner.style.display='none'; if(typeof render==='function') render(); toast('Volviste al inventario actual', true); }); } }); async function loadHistorial(){ const box = document.getElementById('historialList'); if(!box) return; box.innerHTML = '
Cargando...
'; try{ const r = await fetch('/api/inventory/snapshots', { credentials:'include' }); const j = await r.json().catch(()=>({})); if(r.ok && j && Array.isArray(j.items)){ if(!j.items.length){ box.innerHTML = '
Sin historial en el servidor.
'; return; } box.innerHTML = j.items.map(s => { const label = s.label || s.name || ('Snapshot #'+s.id); const ts = s.created_ts ? new Date(s.created_ts*1000).toLocaleString() : (s.when||''); return `
${label} · ${ts}
CSV Excel PDF
`; }).join('\\n'); box.querySelectorAll('button[data-open]').forEach(b=>{ b.addEventListener('click', ()=>{ const id = b.getAttribute('data-open'); const label = (b.closest('.row').querySelector('b')||{}).textContent || ('Snapshot #'+id); viewSnapshot(id, label); }); }); box.querySelectorAll('button[data-paint]').forEach(b=>{ b.addEventListener('click', ()=>{ const id = b.getAttribute('data-paint'); const label = (b.closest('.row').querySelector('b')||{}).textContent || ('Snapshot #'+id); paintSnapshot(id, label); }); }); box.querySelectorAll('button[data-clone]').forEach(b=>{ b.addEventListener('click', ()=>{ const id = b.getAttribute('data-clone'); cloneSnapshotToEditable(id); }); }); return; } }catch(_){} box.innerHTML = '
No fue posible listar el historial.
'; } // === Ultra-conservador: botón "Nuevo inventario" === async function tic911_newInventory(){ try{ // Snapshot del inventario actual (no bloqueante) try{ await saveCurrentInventoryToServer(); }catch(_){} // Preparar Sist=0 usando lógica existente try{ await createZeroInventory(); }catch(_){ startCountFromStock(0); } // Salir de vista historial si estaba activa if(state.view){ state.view.mode='current'; state.view.display=[]; state.view.snapshot=null; } const banner = document.getElementById('histViewBanner'); if(banner) banner.style.display='none'; // Pintar if(typeof render==='function'){ render(); } toast('Nuevo inventario preparado (Sist=0). Carga un CSV o registra entradas.', true); }catch(e){ console.error('tic911_newInventory', e); toast('No se pudo iniciar el nuevo inventario: '+(e.message||e), false); } } document.addEventListener('DOMContentLoaded', ()=>{ const bNew = document.getElementById('btnNewInventory'); if(bNew && !bNew.__ticBind){ bNew.__ticBind = true; bNew.addEventListener('click', tic911_newInventory); } });