PulPOS

Sistema POS para pulperías

¿Qué querés hacer?

Elegí un módulo para trabajar. Todo se abre en una ventana sencilla.

Ventas hoyC$ 0.00
Productos0
Por cobrarC$ 0.00
Por pagarC$ 0.00
Módulo

Vender

Paso 1: agregá productos al carrito. El sistema te va guiando.

🛒 Carrito de venta


Total de esta venta C$ 0.00

💳 Tipo de venta

📝 Nota

Últimas ventas

Productos


Listado de productos


Stock crítico y pedidos a proveedores

Seleccioná los productos bajos de inventario, indicá cantidad a pedir y el sistema agrupa el pedido por proveedor.



Clientes


Listado de clientes

Proveedores


Listado de proveedores

Cuentas por cobrar


Pendientes por cobrar

Cuentas por pagar


Pendientes por pagar

`); w.document.close(); w.focus(); setTimeout(() => w.print(), 300); } function sendTicketWhatsApp() { if (!lastTicketVenta) return; const cliente = state.clientes.find(c => c.id === lastTicketVenta.clienteId); let phone = (cliente?.telefono || '').replace(/\D/g, ''); if (!phone) { phone = prompt('Este cliente no tiene teléfono. Escribí el número de WhatsApp:') || ''; phone = phone.replace(/\D/g, ''); } if (!phone) return toast('No se envió porque falta el número'); if (phone.length === 8) phone = '505' + phone; const url = 'https://wa.me/' + phone + '?text=' + encodeURIComponent(lastTicketText); window.open(url, '_blank'); } function renderVentas() { const rows = state.ventas.slice(0, 10).map(v => ` ${new Date(v.fecha).toLocaleString('es-NI')} ${escapeHtml(v.clienteNombre)} ${v.tipo === 'credito' ? 'Crédito' : 'Contado'} ${money(v.total)} ${v.items.length} producto(s) `).join(''); document.getElementById('tablaVentas').innerHTML = rows ? `
${rows}
FechaClienteTipoTotalDetalle
` : `
Todavía no hay ventas.
`; } function guardarCliente() { const nombre = val('cliNombre'); if (!nombre) return toast('Escribí el nombre del cliente'); const cliente = { id: editClienteId || uid(), nombre, telefono: val('cliTelefono'), direccion: val('cliDireccion') }; if (editClienteId) { state.clientes = state.clientes.map(c => c.id === editClienteId ? cliente : c); toast('Cliente actualizado'); } else { state.clientes.push(cliente); toast('Cliente guardado'); } limpiarCliente(); saveState(); } function editarCliente(id) { const c = state.clientes.find(x => x.id === id); if (!c) return; editClienteId = id; setVal('cliNombre', c.nombre); setVal('cliTelefono', c.telefono); setVal('cliDireccion', c.direccion); openModule('clientes'); } function borrarCliente(id) { const cliente = state.clientes.find(c => c.id === id); if (cliente && cliente.nombre === 'Cliente General') { return toast('No se puede borrar Cliente General'); } if (!confirm('¿Borrar este cliente?')) return; state.clientes = state.clientes.filter(c => c.id !== id); saveState(); toast('Cliente borrado'); } function limpiarCliente() { editClienteId = null; ['cliNombre','cliTelefono','cliDireccion'].forEach(id => setVal(id, '')); } function renderClientes() { const rows = state.clientes.map(c => ` ${escapeHtml(c.nombre)} ${escapeHtml(c.telefono || '-')} ${escapeHtml(c.direccion || '-')} `).join(''); document.getElementById('tablaClientes').innerHTML = `
${rows}
NombreTeléfonoDirecciónAcciones
`; } function renderStockCritico() { const box = document.getElementById('stockCriticoLista'); if (!box) return; state.productos.forEach(p => { if (p.stockMin === undefined) p.stockMin = 5; if (p.proveedorId === undefined) p.proveedorId = ''; }); const criticos = state.productos.filter(p => Number(p.stock || 0) <= Number(p.stockMin ?? 5)); if (!criticos.length) { box.innerHTML = `
No hay productos en stock crítico.
`; document.getElementById('pedidosProveedorResultado').innerHTML = ''; return; } box.innerHTML = `
` + criticos.map(p => { const sugerido = Math.max(1, Number(p.stockMin ?? 5) * 2 - Number(p.stock || 0)); return `
${escapeHtml(p.nombre)}
Stock actual: ${p.stock} · Mínimo: ${p.stockMin ?? 5} · Proveedor: ${escapeHtml(getProveedorNombre(p.proveedorId))}
`; }).join('') + `
`; } function seleccionarStockCritico(valor) { document.querySelectorAll('.pedido-check').forEach(ch => ch.checked = valor); } function generarPedidosProveedor() { const seleccionados = Array.from(document.querySelectorAll('.pedido-check')) .filter(ch => ch.checked) .map(ch => { const id = ch.dataset.id; const producto = state.productos.find(p => p.id === id); const qtyInput = document.querySelector(`.pedido-cantidad[data-id="${id}"]`); const cantidad = Math.max(1, Math.floor(Number(qtyInput?.value || 1))); return { producto, cantidad }; }) .filter(x => x.producto); if (!seleccionados.length) return toast('Seleccioná productos para pedir'); const grupos = {}; seleccionados.forEach(item => { const proveedorId = item.producto.proveedorId || 'sin_proveedor'; if (!grupos[proveedorId]) grupos[proveedorId] = []; grupos[proveedorId].push(item); }); const cards = Object.entries(grupos).map(([proveedorId, items]) => { const proveedor = (state.proveedores || []).find(p => p.id === proveedorId); const nombre = proveedor ? proveedor.nombre : 'Sin proveedor asignado'; const telefono = proveedor ? (proveedor.telefono || '') : ''; const mensaje = buildPedidoMessage(nombre, items); const phone = telefono.replace(/\D/g, ''); const canSend = proveedor && phone; return `

🚚 ${escapeHtml(nombre)}

${canSend ? 'WhatsApp: ' + escapeHtml(telefono) : 'Este proveedor no tiene WhatsApp/teléfono asignado.'}
${canSend ? `` : ''}
`; }).join(''); window._pedidosProveedor = grupos; document.getElementById('pedidoPreview').innerHTML = cards; document.getElementById('pedidoModal').classList.add('open'); } function buildPedidoMessage(nombreProveedor, items) { const lineas = []; lineas.push('Hola' + (nombreProveedor && nombreProveedor !== 'Sin proveedor asignado' ? ' ' + nombreProveedor : '') + ', buenos días.'); lineas.push(''); lineas.push('Por favor me apoyás con este pedido:'); lineas.push(''); items.forEach(i => { lineas.push('• ' + i.producto.nombre + ' — ' + i.cantidad + ' unidad(es)'); }); lineas.push(''); lineas.push('Gracias.'); lineas.push('Pedido generado desde PulPOS.'); return lineas.join('\\n'); } function enviarPedidoWhatsApp(proveedorId) { const proveedor = (state.proveedores || []).find(p => p.id === proveedorId); const items = window._pedidosProveedor?.[proveedorId] || []; if (!proveedor || !items.length) return; let phone = (proveedor.telefono || '').replace(/\D/g, ''); if (!phone) return toast('El proveedor no tiene teléfono'); if (phone.length === 8) phone = '505' + phone; const msg = buildPedidoMessage(proveedor.nombre, items); window.open('https://wa.me/' + phone + '?text=' + encodeURIComponent(msg), '_blank'); } function copiarPedidoProveedor(proveedorId) { const proveedor = (state.proveedores || []).find(p => p.id === proveedorId); const items = window._pedidosProveedor?.[proveedorId] || []; const nombre = proveedor ? proveedor.nombre : 'Sin proveedor asignado'; const msg = buildPedidoMessage(nombre, items); navigator.clipboard?.writeText(msg).then(() => { toast('Pedido copiado'); }).catch(() => { prompt('Copiá el pedido:', msg); }); } function closePedidoModal() { document.getElementById('pedidoModal').classList.remove('open'); } function guardarProveedor() { const nombre = val('provNombre'); if (!nombre) return toast('Escribí el nombre del proveedor'); const proveedor = { id: editProveedorId || uid(), nombre, telefono: val('provTelefono'), direccion: val('provDireccion'), nota: val('provNota') }; if (editProveedorId) { state.proveedores = state.proveedores.map(p => p.id === editProveedorId ? proveedor : p); toast('Proveedor actualizado'); } else { state.proveedores.push(proveedor); toast('Proveedor guardado'); } limpiarProveedor(); saveState(); renderProveedoresSelect(); } function editarProveedor(id) { const p = state.proveedores.find(x => x.id === id); if (!p) return; editProveedorId = id; setVal('provNombre', p.nombre); setVal('provTelefono', p.telefono); setVal('provDireccion', p.direccion); setVal('provNota', p.nota); openModule('proveedores'); } function borrarProveedor(id) { if (!confirm('¿Borrar este proveedor?')) return; state.proveedores = state.proveedores.filter(p => p.id !== id); saveState(); toast('Proveedor borrado'); } function limpiarProveedor() { editProveedorId = null; ['provNombre','provTelefono','provDireccion','provNota'].forEach(id => setVal(id, '')); } function renderProveedores() { if (!state.proveedores) state.proveedores = []; const rows = state.proveedores.map(p => ` ${escapeHtml(p.nombre)}
${escapeHtml(p.nota || '-')} ${escapeHtml(p.telefono || '-')} ${escapeHtml(p.direccion || '-')} `).join(''); document.getElementById('tablaProveedores').innerHTML = rows ? `
${rows}
ProveedorTeléfonoDirecciónAcciones
` : `
No hay proveedores registrados.
`; } function normalizarCuenta(cuenta) { if (!cuenta.abonos) cuenta.abonos = []; if (cuenta.saldo === undefined || cuenta.saldo === null) { const abonado = cuenta.abonos.reduce((s, a) => s + Number(a.monto || 0), 0); cuenta.saldo = Math.max(0, Number(cuenta.monto || 0) - abonado); } if (Number(cuenta.saldo || 0) <= 0) cuenta.estado = 'pagado'; return cuenta; } function totalPendienteCuenta(cuenta) { normalizarCuenta(cuenta); return Number(cuenta.saldo || 0); } function abrirAbono(tipo, id) { const lista = tipo === 'cxc' ? state.cxc : state.cxp; const cuenta = lista.find(x => x.id === id); if (!cuenta) return; normalizarCuenta(cuenta); document.getElementById('abonoTipo').value = tipo; document.getElementById('abonoId').value = id; document.getElementById('abonoMonto').value = cuenta.saldo; document.getElementById('abonoNota').value = ''; const nombre = tipo === 'cxc' ? cuenta.clienteNombre : cuenta.proveedor; document.getElementById('abonoTitulo').textContent = tipo === 'cxc' ? 'Registrar abono recibido' : 'Registrar abono pagado'; document.getElementById('abonoInfo').innerHTML = `
${escapeHtml(nombre)}
Saldo pendiente: ${money(cuenta.saldo)}
`; document.getElementById('abonoModal').classList.add('open'); } function closeAbonoModal() { document.getElementById('abonoModal').classList.remove('open'); } function guardarAbono() { const tipo = val('abonoTipo'); const id = val('abonoId'); const monto = Number(document.getElementById('abonoMonto').value || 0); const nota = val('abonoNota'); if (monto <= 0) return toast('El abono debe ser mayor que cero'); const lista = tipo === 'cxc' ? state.cxc : state.cxp; const cuenta = lista.find(x => x.id === id); if (!cuenta) return; normalizarCuenta(cuenta); if (monto > cuenta.saldo) { return toast('El abono no puede ser mayor al saldo'); } cuenta.abonos.push({ id: uid(), fecha: new Date().toISOString(), monto, nota }); cuenta.saldo = Math.max(0, Number(cuenta.saldo || 0) - monto); if (cuenta.saldo <= 0) { cuenta.estado = 'pagado'; } else { cuenta.estado = 'pendiente'; } closeAbonoModal(); saveState(); toast('Abono registrado'); } function renderAbonos(cuenta) { normalizarCuenta(cuenta); if (!cuenta.abonos.length) return ''; return `
Abonos:
${cuenta.abonos.map(a => `${new Date(a.fecha).toLocaleDateString('es-NI')} — ${money(a.monto)}${a.nota ? ' · ' + escapeHtml(a.nota) : ''}`).join('
')}
`; } function guardarCXC() { const clienteId = val('cxcCliente'); const cliente = state.clientes.find(c => c.id === clienteId); const monto = num('cxcMonto'); if (!cliente || monto <= 0) return toast('Seleccioná cliente y monto válido'); state.cxc.unshift({ id: uid(), clienteId, clienteNombre: cliente.nombre, concepto: val('cxcConcepto') || 'Cuenta por cobrar', monto, fecha: val('cxcFecha') || today(), estado: 'pendiente' }); setVal('cxcMonto', 0); setVal('cxcConcepto', ''); setVal('cxcFecha', ''); saveState(); toast('Cuenta por cobrar guardada'); } function pagarCXC(id) { const cuenta = state.cxc.find(x => x.id === id); if (!cuenta) return; normalizarCuenta(cuenta); if (cuenta.saldo > 0) { cuenta.abonos.push({ id: uid(), fecha: new Date().toISOString(), monto: cuenta.saldo, nota: 'Pago total' }); } cuenta.saldo = 0; cuenta.estado = 'pagado'; saveState(); toast('Cuenta marcada como pagada'); } function borrarCXC(id) { if (!confirm('¿Borrar esta cuenta?')) return; state.cxc = state.cxc.filter(x => x.id !== id); saveState(); } function renderCXC() { state.cxc.forEach(normalizarCuenta); const rows = state.cxc.map(x => ` ${escapeHtml(x.clienteNombre)}
${escapeHtml(x.concepto)}${renderAbonos(x)} ${money(x.saldo)}
Original: ${money(x.monto)} ${escapeHtml(x.fecha || '-')} ${x.estado === 'pagado' ? 'Pagado' : 'Pendiente'} ${x.estado !== 'pagado' ? ` ` : ''} `).join(''); document.getElementById('tablaCXC').innerHTML = rows ? `
${rows}
ClienteSaldoFecha / venceEstadoAcciones
` : `
No hay cuentas por cobrar.
`; } function guardarCXP() { const proveedor = val('cxpProveedor'); const monto = num('cxpMonto'); if (!proveedor || monto <= 0) return toast('Escribí proveedor y monto válido'); state.cxp.unshift({ id: uid(), proveedor, concepto: val('cxpConcepto') || 'Cuenta por pagar', monto, fecha: val('cxpFecha') || today(), estado: 'pendiente' }); setVal('cxpProveedor', ''); setVal('cxpMonto', 0); setVal('cxpConcepto', ''); setVal('cxpFecha', ''); saveState(); toast('Cuenta por pagar guardada'); } function pagarCXP(id) { const cuenta = state.cxp.find(x => x.id === id); if (!cuenta) return; normalizarCuenta(cuenta); if (cuenta.saldo > 0) { cuenta.abonos.push({ id: uid(), fecha: new Date().toISOString(), monto: cuenta.saldo, nota: 'Pago total' }); } cuenta.saldo = 0; cuenta.estado = 'pagado'; saveState(); toast('Cuenta marcada como pagada'); } function borrarCXP(id) { if (!confirm('¿Borrar esta cuenta?')) return; state.cxp = state.cxp.filter(x => x.id !== id); saveState(); } function renderCXP() { state.cxp.forEach(normalizarCuenta); const rows = state.cxp.map(x => ` ${escapeHtml(x.proveedor)}
${escapeHtml(x.concepto)}${renderAbonos(x)} ${money(x.saldo)}
Original: ${money(x.monto)} ${escapeHtml(x.fecha || '-')} ${x.estado === 'pagado' ? 'Pagado' : 'Pendiente'} ${x.estado !== 'pagado' ? ` ` : ''} `).join(''); document.getElementById('tablaCXP').innerHTML = rows ? `
${rows}
ProveedorSaldoFecha / venceEstadoAcciones
` : `
No hay cuentas por pagar.
`; } function exportData() { const data = JSON.stringify(state, null, 2); navigator.clipboard?.writeText(data).then(() => { toast('Respaldo copiado al portapapeles'); }).catch(() => { alert(data); }); } function openBackup() { document.getElementById('backupModal').classList.add('open'); } function closeBackup() { document.getElementById('backupModal').classList.remove('open'); } function importData() { try { const data = JSON.parse(document.getElementById('backupText').value); if (!data.productos || !data.clientes) throw new Error(); state = Object.assign(initialState(), data); if (!state.proveedores) state.proveedores = []; saveState(); closeBackup(); toast('Respaldo importado'); } catch (e) { toast('El respaldo no es válido'); } } function val(id) { return (document.getElementById(id)?.value || '').trim(); } function setVal(id, value) { const el = document.getElementById(id); if (el) el.value = value ?? ''; } function num(id) { return Number(document.getElementById(id)?.value || 0); } function escapeHtml(text) { return String(text ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } renderAll(); if ('serviceWorker' in navigator) { const sw = ` const CACHE = 'erp-pulperia-cache-v1'; self.addEventListener('install', e => { e.waitUntil(caches.open(CACHE).then(c => c.addAll(['./']))); }); self.addEventListener('fetch', e => { e.respondWith(caches.match(e.request).then(r => r || fetch(e.request))); }); `; const blob = new Blob([sw], { type: 'text/javascript' }); const url = URL.createObjectURL(blob); navigator.serviceWorker.register(url).catch(() => {}); }