diff --git a/pos/static/js/invoicing.js b/pos/static/js/invoicing.js new file mode 100644 index 0000000..72d55d2 --- /dev/null +++ b/pos/static/js/invoicing.js @@ -0,0 +1,227 @@ +// /home/Autopartes/pos/static/js/invoicing.js +// Invoicing module: CFDI queue management, cancel, PDF + +const Invoicing = (() => { + const API = '/pos/api/invoicing'; + + function token() { + return localStorage.getItem('pos_token') || ''; + } + + function headers() { + return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' }; + } + + async function api(path, opts = {}) { + const res = await fetch(`${API}${path}`, { headers: headers(), ...opts }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || 'Request failed'); + } + return res.json(); + } + + function fmt(n) { + return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function badgeClass(status) { + return { + pending: 'badge-pending', + sending: 'badge-sending', + stamped: 'badge-stamped', + failed: 'badge-failed', + cancelled: 'badge-cancelled', + }[status] || ''; + } + + function badgeLabel(status) { + return { + pending: 'Pendiente', + sending: 'Enviando', + stamped: 'Timbrado', + failed: 'Fallido', + cancelled: 'Cancelado', + }[status] || status; + } + + // ─── Queue List ──────────────────────────────── + + async function loadQueue() { + try { + const status = document.getElementById('filter-status').value; + const type = document.getElementById('filter-type').value; + let qs = '?per_page=50'; + if (status) qs += `&status=${status}`; + if (type) qs += `&type=${type}`; + + const res = await api(`/queue${qs}`); + renderQueue(res.data || []); + updateStats(res.data || []); + } catch (e) { + document.getElementById('queue-list').innerHTML = + `

Error: ${e.message}

`; + } + } + + function updateStats(items) { + const counts = { pending: 0, sending: 0, stamped: 0, failed: 0, cancelled: 0 }; + items.forEach(i => { if (counts[i.status] !== undefined) counts[i.status]++; }); + + document.getElementById('queue-stats').innerHTML = ` +
${counts.pending}
Pendientes
+
${counts.sending}
Enviando
+
${counts.stamped}
Timbrados
+
${counts.failed}
Fallidos
+
${counts.cancelled}
Cancelados
`; + } + + function renderQueue(items) { + const container = document.getElementById('queue-list'); + if (!items.length) { container.innerHTML = '

No hay CFDIs en la cola.

'; return; } + + let html = ` + + + + `; + + for (const item of items) { + const uuid = item.uuid_fiscal + ? `${item.uuid_fiscal.substring(0, 8)}...` + : '-'; + html += ` + + + + + + + + + + `; + } + html += '
#VentaTipoFolioUUIDEstadoReintentosFechaAcciones
${item.id}#${item.sale_id}${item.type}${item.provisional_folio || '-'}${uuid}${badgeLabel(item.status)}${item.retry_count || 0}${item.created_at ? new Date(item.created_at).toLocaleDateString('es-MX') : ''} + + ${item.status === 'stamped' ? `` : ''} + ${item.sale_id ? `PDF` : ''} +
'; + container.innerHTML = html; + } + + // ─── Detail ──────────────────────────────────── + + async function showDetail(cfdiId) { + try { + const item = await api(`/queue/${cfdiId}`); + let html = `

CFDI #${item.id}

+
+
#${item.sale_id}
+
${item.type}
+
${badgeLabel(item.status)}
+
${item.provisional_folio || '-'}
+
${item.uuid_fiscal || '-'}
+
${item.retry_count}
+
${item.created_at || '-'}
+
${item.stamped_at || '-'}
+
`; + + if (item.error_message) { + html += `

Error: ${item.error_message}

`; + } + if (item.cancel_motive) { + html += `

Motivo cancelacion: ${item.cancel_motive}

`; + } + + // XML preview + const xml = item.xml_signed || item.xml_unsigned; + if (xml) { + html += `

XML

${escapeHtml(xml)}
`; + } + + document.getElementById('detail-content').innerHTML = html; + document.getElementById('detail-modal').classList.add('active'); + } catch (e) { + alert('Error: ' + e.message); + } + } + + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // ─── Process Queue ───────────────────────────── + + async function processQueue() { + if (!confirm('Procesar todos los CFDIs pendientes?')) return; + try { + const result = await api('/queue/process', { method: 'POST' }); + alert(`Procesados: ${result.processed}, Timbrados: ${result.stamped}, Fallidos: ${result.failed}`); + loadQueue(); + } catch (e) { + alert('Error: ' + e.message); + } + } + + // ─── Cancel ──────────────────────────────────── + + function showCancelModal(cfdiId) { + document.getElementById('cancel-cfdi-id').value = cfdiId; + document.getElementById('cancel-motive').value = ''; + document.getElementById('cancel-replacement-uuid').value = ''; + document.getElementById('replacement-uuid-group').style.display = 'none'; + document.getElementById('cancel-modal').classList.add('active'); + } + + function onMotiveChange() { + const motive = document.getElementById('cancel-motive').value; + document.getElementById('replacement-uuid-group').style.display = + motive === '01' ? 'block' : 'none'; + } + + async function confirmCancel() { + const cfdiId = document.getElementById('cancel-cfdi-id').value; + const motive = document.getElementById('cancel-motive').value; + const replacementUuid = document.getElementById('cancel-replacement-uuid').value; + + if (!motive) { alert('Selecciona un motivo de cancelacion.'); return; } + if (motive === '01' && !replacementUuid) { alert('UUID sustituto requerido para motivo 01.'); return; } + + if (!confirm('Confirmar cancelacion ante el SAT?')) return; + + try { + const body = { motive }; + if (replacementUuid) body.replacement_uuid = replacementUuid; + + await api(`/cancel/${cfdiId}`, { method: 'POST', body: JSON.stringify(body) }); + closeModal('cancel-modal'); + loadQueue(); + alert('CFDI cancelado exitosamente.'); + } catch (e) { + alert('Error: ' + e.message); + } + } + + // ─── Modal helpers ───────────────────────────── + + function closeModal(id) { + document.getElementById(id).classList.remove('active'); + } + + // ─── Init ────────────────────────────────────── + + document.addEventListener('DOMContentLoaded', () => { + loadQueue(); + }); + + return { + loadQueue, processQueue, showDetail, showCancelModal, + onMotiveChange, confirmCancel, closeModal, + }; +})(); diff --git a/pos/templates/invoicing.html b/pos/templates/invoicing.html new file mode 100644 index 0000000..78c6c60 --- /dev/null +++ b/pos/templates/invoicing.html @@ -0,0 +1,133 @@ + + + + + + Facturacion - Nexus POS + + + + +
+
+

Facturacion CFDI

+
+ + POS + Contabilidad +
+
+ + +
+ + +
+ + + +
+ + +
+
+
+ + + + + + + + + +