feat(pos): migrate CFDI timbrado from Horux to Facturapi
- Add Facturapi REST service (invoices, customers, orgs, cancel, downloads) - Add JSON payload builder for ingreso/egreso/pago/global invoices - Replace XML queue with Facturapi JSON queue (payload_unsigned, external_id) - Update invoicing blueprint with Facturapi config and download endpoints - Update global invoice service to use Facturapi payloads - Add migration v4.3_facturapi.sql and tenant rollout script - Update invoicing UI: payload preview, PDF/XML downloads, PAC status panel - Add FACTURAPI_USER_KEY to .env.example
This commit is contained in:
@@ -62,6 +62,7 @@ const Invoicing = (() => {
|
||||
if (name === 'notas') loadNotas();
|
||||
if (name === 'complementos') loadComplementos();
|
||||
if (name === 'cancelaciones') loadCancelaciones();
|
||||
if (name === 'config') loadFacturapiStatus();
|
||||
}
|
||||
|
||||
// ---- Badge helpers ----
|
||||
@@ -259,6 +260,35 @@ const Invoicing = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Facturapi status (config tab) ----
|
||||
async function loadFacturapiStatus() {
|
||||
const container = document.getElementById('facturapi-status');
|
||||
if (!container) return;
|
||||
container.innerHTML = 'Cargando...';
|
||||
try {
|
||||
const status = await api('/facturapi/status');
|
||||
if (!status.configured) {
|
||||
container.innerHTML = `<p style="color:var(--color-error);">Facturapi no configurado. Configura la llave API en Configuración.</p>`;
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);">
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">Organización</div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">${status.legal_name || '-'}</div>
|
||||
<div style="font-family:var(--font-mono);font-size:var(--text-body-sm);">${status.tax_id || ''}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">CSD</div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">${status.has_csd ? '<span style="color:var(--color-success);">Activo</span>' : '<span style="color:var(--color-error);">Pendiente</span>'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (e) {
|
||||
container.innerHTML = `<p style="color:var(--color-error);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Detail modal (uses modalDetalleOverlay) ----
|
||||
async function showDetail(cfdiId) {
|
||||
const overlay = document.getElementById('modalDetalleOverlay');
|
||||
@@ -300,10 +330,16 @@ const Invoicing = (() => {
|
||||
</div>
|
||||
</div>
|
||||
${item.error_message ? `<p style="color:var(--color-error);margin-bottom:var(--space-3);"><strong>Error:</strong> ${escapeHtml(item.error_message)}</p>` : ''}
|
||||
${(item.xml_signed || item.xml_unsigned) ? `
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">Vista previa XML</div>
|
||||
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">${escapeHtml(item.xml_signed || item.xml_unsigned)}</pre>
|
||||
` : ''}`;
|
||||
${(item.xml_signed || item.payload_unsigned) ? `
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">${item.xml_signed ? 'Vista previa XML' : 'Payload Facturapi'}</div>
|
||||
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">${escapeHtml(item.xml_signed || item.payload_unsigned)}</pre>
|
||||
` : ''}
|
||||
${item.status === 'stamped' && item.external_id ? `
|
||||
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-4);">
|
||||
<a class="btn btn--ghost btn--sm" href="${API}/facturapi/download/${item.id}/xml" target="_blank">Descargar XML</a>
|
||||
<a class="btn btn--ghost btn--sm" href="${API}/facturapi/download/${item.id}/pdf" target="_blank">Descargar PDF</a>
|
||||
</div>
|
||||
` : ''}
|
||||
}
|
||||
|
||||
// Wire the cancel button inside modal footer
|
||||
@@ -531,7 +567,7 @@ const Invoicing = (() => {
|
||||
window.notaCreditoPlaceholder = notaCreditoPlaceholder;
|
||||
|
||||
return {
|
||||
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones,
|
||||
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones, loadFacturapiStatus,
|
||||
showDetail, showCancelModal, confirmCancel, processQueue,
|
||||
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
|
||||
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice,
|
||||
|
||||
Reference in New Issue
Block a user