diff --git a/apps/web/components/cfdi/cfdi-invoice.tsx b/apps/web/components/cfdi/cfdi-invoice.tsx index 6e2d538..d361c1b 100644 --- a/apps/web/components/cfdi/cfdi-invoice.tsx +++ b/apps/web/components/cfdi/cfdi-invoice.tsx @@ -30,12 +30,21 @@ const formatDate = (dateString: string) => year: 'numeric', }); +const formatDateTime = (dateString: string) => + new Date(dateString).toLocaleString('es-MX', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + const tipoLabels: Record = { ingreso: 'Ingreso', egreso: 'Egreso', traslado: 'Traslado', pago: 'Pago', - nomina: 'Nomina', + nomina: 'Nómina', }; const formaPagoLabels: Record = { @@ -52,174 +61,252 @@ const metodoPagoLabels: Record = { PPD: 'Pago en parcialidades o diferido', }; +const usoCfdiLabels: Record = { + G01: 'Adquisición de mercancías', + G02: 'Devoluciones, descuentos o bonificaciones', + G03: 'Gastos en general', + I01: 'Construcciones', + I02: 'Mobilario y equipo de oficina', + I03: 'Equipo de transporte', + I04: 'Equipo de cómputo', + I05: 'Dados, troqueles, moldes', + I06: 'Comunicaciones telefónicas', + I07: 'Comunicaciones satelitales', + I08: 'Otra maquinaria y equipo', + D01: 'Honorarios médicos', + D02: 'Gastos médicos por incapacidad', + D03: 'Gastos funerales', + D04: 'Donativos', + D05: 'Intereses por créditos hipotecarios', + D06: 'Aportaciones voluntarias SAR', + D07: 'Primas por seguros de gastos médicos', + D08: 'Gastos de transportación escolar', + D09: 'Depósitos en cuentas para el ahorro', + D10: 'Pagos por servicios educativos', + P01: 'Por definir', + S01: 'Sin efectos fiscales', + CP01: 'Pagos', + CN01: 'Nómina', +}; + export const CfdiInvoice = forwardRef( ({ cfdi, conceptos }, ref) => { return (
- {/* Header */} -
-
- [LOGO] + {/* Header con gradiente */} +
+
+
+

Emisor

+

{cfdi.nombreEmisor}

+

RFC: {cfdi.rfcEmisor}

+
+
+
+ + {cfdi.estado === 'vigente' ? 'VIGENTE' : 'CANCELADO'} + + + {tipoLabels[cfdi.tipo] || cfdi.tipo} + +
+
+ {cfdi.serie && {cfdi.serie}-} + {cfdi.folio || 'S/N'} +
+

{formatDate(cfdi.fechaEmision)}

+
-
-

FACTURA

-

- {cfdi.serie && `Serie: ${cfdi.serie} `} - {cfdi.folio && `Folio: ${cfdi.folio}`} +

+ +
+ {/* Receptor */} +
+
+
+

Receptor

+

{cfdi.nombreReceptor}

+

RFC: {cfdi.rfcReceptor}

+
+ {cfdi.usoCfdi && ( +
+

Uso CFDI

+

+ {cfdi.usoCfdi} - {usoCfdiLabels[cfdi.usoCfdi] || ''} +

+
+ )} +
+
+ + {/* Datos del Comprobante */} +
+
+

Método Pago

+

+ {cfdi.metodoPago || '-'} +

+

+ {cfdi.metodoPago ? metodoPagoLabels[cfdi.metodoPago] || '' : ''} +

+
+
+

Forma Pago

+

+ {cfdi.formaPago || '-'} +

+

+ {cfdi.formaPago ? formaPagoLabels[cfdi.formaPago] || '' : ''} +

+
+
+

Moneda

+

{cfdi.moneda || 'MXN'}

+ {cfdi.tipoCambio && cfdi.tipoCambio !== 1 && ( +

TC: {cfdi.tipoCambio}

+ )} +
+
+

Versión

+

CFDI 4.0

+
+
+ + {/* Conceptos */} + {conceptos && conceptos.length > 0 && ( +
+

+ + Conceptos +

+
+ + + + + + + + + + + {conceptos.map((concepto, idx) => ( + + + + + + + ))} + +
DescripciónCant.P. UnitarioImporte
+

{concepto.descripcion}

+ {concepto.claveProdServ && ( +

+ Clave: {concepto.claveProdServ} + {concepto.claveUnidad && ` | Unidad: ${concepto.claveUnidad}`} +

+ )} +
{concepto.cantidad} + {formatCurrency(concepto.valorUnitario)} + + {formatCurrency(concepto.importe)} +
+
+
+ )} + + {/* Totales */} +
+
+
+
+ Subtotal + {formatCurrency(cfdi.subtotal)} +
+ {cfdi.descuento > 0 && ( +
+ Descuento + -{formatCurrency(cfdi.descuento)} +
+ )} + {cfdi.iva > 0 && ( +
+ IVA (16%) + {formatCurrency(cfdi.iva)} +
+ )} + {cfdi.ivaRetenido > 0 && ( +
+ IVA Retenido + -{formatCurrency(cfdi.ivaRetenido)} +
+ )} + {cfdi.isrRetenido > 0 && ( +
+ ISR Retenido + -{formatCurrency(cfdi.isrRetenido)} +
+ )} +
+
+ TOTAL + {formatCurrency(cfdi.total)} +
+
+
+ + {/* Timbre Fiscal Digital */} +
+
+ {/* QR Placeholder */} +
+
+ + + + QR +
+
+ + {/* Info del Timbre */} +
+

+ + + + Timbre Fiscal Digital +

+
+
+ UUID: + {cfdi.uuidFiscal} +
+
+ Fecha de Timbrado: + {formatDateTime(cfdi.fechaTimbrado)} +
+
+
+
+ + {/* Leyenda */} +

+ Este documento es una representación impresa de un CFDI • Verificable en: https://verificacfdi.facturaelectronica.sat.gob.mx

-

Fecha: {formatDate(cfdi.fechaEmision)}

- - {cfdi.estado === 'vigente' ? 'VIGENTE' : 'CANCELADO'} - -
-
- - {/* Emisor / Receptor */} -
-
-

- EMISOR -

-

{cfdi.nombreEmisor}

-

RFC: {cfdi.rfcEmisor}

-
-
-

- RECEPTOR -

-

{cfdi.nombreReceptor}

-

RFC: {cfdi.rfcReceptor}

- {cfdi.usoCfdi && ( -

Uso CFDI: {cfdi.usoCfdi}

- )} -
-
- - {/* Datos del Comprobante */} -
-

- DATOS DEL COMPROBANTE -

-
-
- Tipo: -

{tipoLabels[cfdi.tipo] || cfdi.tipo}

-
-
- Método de Pago: -

- {cfdi.metodoPago ? metodoPagoLabels[cfdi.metodoPago] || cfdi.metodoPago : '-'} -

-
-
- Forma de Pago: -

- {cfdi.formaPago ? formaPagoLabels[cfdi.formaPago] || cfdi.formaPago : '-'} -

-
-
- Moneda: -

- {cfdi.moneda} - {cfdi.tipoCambio !== 1 && ` (TC: ${cfdi.tipoCambio})`} -

-
-
-
- - {/* Conceptos */} - {conceptos && conceptos.length > 0 && ( -
-

- CONCEPTOS -

- - - - - - - - - - - {conceptos.map((concepto, idx) => ( - - - - - - - ))} - -
DescripciónCant.P. Unit.Importe
{concepto.descripcion}{concepto.cantidad} - {formatCurrency(concepto.valorUnitario)} - - {formatCurrency(concepto.importe)} -
-
- )} - - {/* Totales */} -
-
-
- Subtotal: - {formatCurrency(cfdi.subtotal)} -
- {cfdi.descuento > 0 && ( -
- Descuento: - -{formatCurrency(cfdi.descuento)} -
- )} - {cfdi.iva > 0 && ( -
- IVA (16%): - {formatCurrency(cfdi.iva)} -
- )} - {cfdi.ivaRetenido > 0 && ( -
- IVA Retenido: - -{formatCurrency(cfdi.ivaRetenido)} -
- )} - {cfdi.isrRetenido > 0 && ( -
- ISR Retenido: - -{formatCurrency(cfdi.isrRetenido)} -
- )} -
- TOTAL: - {formatCurrency(cfdi.total)} -
-
-
- - {/* Timbre Fiscal */} -
-

TIMBRE FISCAL DIGITAL

-
-
-

UUID:

-

{cfdi.uuidFiscal}

-
-
-

Fecha de Timbrado:

-

{cfdi.fechaTimbrado}

-
diff --git a/scripts/update-cfdi-xml.js b/scripts/update-cfdi-xml.js new file mode 100644 index 0000000..de65580 --- /dev/null +++ b/scripts/update-cfdi-xml.js @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/** + * Script para actualizar CFDIs existentes con su XML original + * Uso: node scripts/update-cfdi-xml.js + * Ejemplo: node scripts/update-cfdi-xml.js /root/xmls tenant_roem691011ez4 + */ + +const fs = require('fs'); +const path = require('path'); +const { Pool } = require('pg'); + +// Configuración de la base de datos +const pool = new Pool({ + host: 'localhost', + port: 5432, + database: 'horux360', + user: 'postgres', + password: 'postgres', +}); + +// Extraer UUID del XML +function extractUuidFromXml(xmlContent) { + // Buscar UUID en TimbreFiscalDigital + const uuidMatch = xmlContent.match(/UUID=["']([A-Fa-f0-9-]{36})["']/i); + return uuidMatch ? uuidMatch[1].toUpperCase() : null; +} + +// Procesar archivos en lotes +async function processFiles(directory, schema, batchSize = 500) { + const files = fs.readdirSync(directory).filter(f => f.toLowerCase().endsWith('.xml')); + + console.log(`\nEncontrados ${files.length} archivos XML en ${directory}`); + console.log(`Schema: ${schema}`); + console.log(`Tamaño de lote: ${batchSize}\n`); + + let updated = 0; + let notFound = 0; + let errors = 0; + let processed = 0; + + const startTime = Date.now(); + + // Procesar en lotes + for (let i = 0; i < files.length; i += batchSize) { + const batch = files.slice(i, i + batchSize); + const updates = []; + + // Leer y parsear XMLs del lote + for (const file of batch) { + try { + const filePath = path.join(directory, file); + const xmlContent = fs.readFileSync(filePath, 'utf8'); + const uuid = extractUuidFromXml(xmlContent); + + if (uuid) { + updates.push({ uuid, xmlContent }); + } else { + errors++; + if (errors <= 5) { + console.log(` ⚠ No se encontró UUID en: ${file}`); + } + } + } catch (err) { + errors++; + if (errors <= 5) { + console.log(` ✗ Error leyendo ${file}: ${err.message}`); + } + } + } + + // Actualizar base de datos en una transacción + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + for (const { uuid, xmlContent } of updates) { + const result = await client.query( + `UPDATE "${schema}".cfdis SET xml_original = $1 WHERE UPPER(uuid_fiscal) = $2 AND xml_original IS NULL`, + [xmlContent, uuid] + ); + + if (result.rowCount > 0) { + updated++; + } else { + notFound++; + } + } + + await client.query('COMMIT'); + } catch (err) { + await client.query('ROLLBACK'); + console.error(`Error en lote: ${err.message}`); + errors += batch.length; + } finally { + client.release(); + } + + processed += batch.length; + + // Progreso + const percent = ((processed / files.length) * 100).toFixed(1); + const elapsed = ((Date.now() - startTime) / 1000).toFixed(0); + const rate = (processed / elapsed).toFixed(0); + process.stdout.write(`\r Procesando: ${processed}/${files.length} (${percent}%) - ${updated} actualizados - ${rate} archivos/seg `); + } + + const totalTime = ((Date.now() - startTime) / 1000).toFixed(1); + + console.log(`\n\n✓ Completado en ${totalTime} segundos`); + console.log(` - Actualizados: ${updated}`); + console.log(` - No encontrados (UUID no existe en BD): ${notFound}`); + console.log(` - Errores: ${errors}`); + + await pool.end(); +} + +// Main +const args = process.argv.slice(2); +if (args.length < 2) { + console.log('Uso: node scripts/update-cfdi-xml.js '); + console.log('Ejemplo: node scripts/update-cfdi-xml.js /root/xmls tenant_roem691011ez4'); + process.exit(1); +} + +const [directory, schema] = args; + +if (!fs.existsSync(directory)) { + console.error(`Error: El directorio ${directory} no existe`); + process.exit(1); +} + +processFiles(directory, schema).catch(err => { + console.error('Error fatal:', err); + process.exit(1); +});