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.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 = `
+
+ | # | Venta | Tipo | Folio |
+ UUID | Estado | Reintentos | Fecha | Acciones |
+
`;
+
+ for (const item of items) {
+ const uuid = item.uuid_fiscal
+ ? `${item.uuid_fiscal.substring(0, 8)}...`
+ : '-';
+ html += `
+ | ${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` : ''}
+ |
+
`;
+ }
+ html += '
';
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Cancelar CFDI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+