Claro, he completado y mejorado significativamente el script para que funcione con el HTML proporcionado. El script ahora es completamente funcional, está mejor estructurado, es más fácil de leer y mantener. He añadido comentarios para explicar las partes clave y he implementado toda la lógica necesaria para que la interfaz sea dinámica. Cambios y Mejoras Principales: * Estructura del Código: Organizado en secciones claras: Selección de Elementos del DOM, Datos Iniciales, Funciones Principales, Lógica de Negocio (cálculos, generación de datos), Generadores (XML, PDF) y Event Listeners. * Funcionalidad Completa: * Añadir y Eliminar Conceptos: Puedes agregar nuevas filas de conceptos y eliminarlas dinámicamente. * Cálculos Automáticos: El subtotal, IVA y total se recalculan automáticamente cada vez que cambias la cantidad o el precio de un artículo. * "Timbrado" Simulado: El botón "Timbrar y Descargar" genera la previsualización del XML, actualiza los totales y descarga un PDF profesional de la factura. * Reseteo del Formulario: El botón "Nueva Factura" limpia todos los campos y te deja listo para empezar de nuevo. * Generación de PDF Mejorada: El PDF ahora luce mucho más profesional, incluyendo los datos del emisor, receptor, una tabla de conceptos bien formateada y los totales desglosados. * Generación de XML: Se genera un XML de CFDI 4.0 simulado y válido en estructura, que se muestra en la previsualización. * Inicialización: El formulario se carga con datos de ejemplo y la fecha y hora actuales para agilizar el uso. 1. Código HTML (Completo) Este es el HTML que proporcionaste. No necesita cambios. Generador de Factura Simulado

Factura

Folio:

Datos del Receptor

Detalles de la Factura

Descripción Clave SAT Cant. Unidad P. Unitario Importe
Subtotal: $0.00
IVA (16%): $0.00
Total: $0.00

Acciones de Factura

Nota: El "Timbrado" es una simulación. Esta herramienta no genera facturas con validez fiscal.

Previsualización de XML (Simulado)

Presiona "Timbrar y Descargar" para generar el XML...
2. Código JavaScript (Mejorado y Completo) Guarda este código como invoice.js en la misma carpeta que tu archivo HTML. document.addEventListener('DOMContentLoaded', () => { // --- 1. SELECCIÓN DE ELEMENTOS DEL DOM --- // Se obtienen todas las referencias a los elementos HTML para no tener que buscarlos repetidamente. const folioEl = document.getElementById('folio'); const nombreEmisorEl = document.getElementById('nombreEmisor'); const rfcEmisorEl = document.getElementById('rfcEmisor'); const direccionEmisorEl = document.getElementById('direccionEmisor'); const nombreClienteEl = document.getElementById('nombreCliente'); const rfcClienteEl = document.getElementById('rfcCliente'); const fechaEl = document.getElementById('fecha'); const usoCfdiEl = document.getElementById('usoCfdi'); const exportKeyEl = document.getElementById('export-key'); const paymentMethodEl = document.getElementById('payment-method'); const paymentFormEl = document.getElementById('payment-form'); const itemRows = document.getElementById('item-rows'); const addItemBtn = document.getElementById('addItemBtn'); const subtotalEl = document.getElementById('subtotal'); const ivaTotalEl = document.getElementById('ivaTotal'); const totalEl = document.getElementById('total'); const stampBtn = document.getElementById('stampBtn'); const resetBtn = document.getElementById('resetBtn'); const xmlOutputEl = document.getElementById('xml-output'); // --- 2. DATOS INICIALES Y CONSTANTES --- const IVA_RATE = 0.16; // Tasa de IVA al 16% const emisorData = { nombre: 'Mi Empresa S.A. de C.V.', rfc: 'EME010101ABC', direccion: 'Av. Siempre Viva 742, Springfield, CP 12345', regimenFiscal: '601' // General de Ley Personas Morales }; // --- 3. FUNCIONES AUXILIARES --- /** * Formatea un número como moneda en formato mexicano (MXN). * @param {number} amount - La cantidad a formatear. * @returns {string} La cantidad formateada como cadena de texto (e.g., "$1,234.50"). */ const formatCurrency = (amount) => { return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(amount); }; /** * Genera un UUID v4 simulado para el timbre fiscal. * @returns {string} Un UUID. */ const generateUUID = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; // --- 4. LÓGICA DE NEGOCIO --- /** * Calcula los totales (subtotal, IVA, total) basándose en los conceptos actuales. * Actualiza la interfaz de usuario con los nuevos valores. */ const calculateTotals = () => { let subtotal = 0; const rows = itemRows.querySelectorAll('.item-row'); rows.forEach(row => { const quantity = parseFloat(row.querySelector('.quantity').value) || 0; const unitPrice = parseFloat(row.querySelector('.unit-price').value) || 0; const importe = quantity * unitPrice; row.querySelector('.importe').textContent = formatCurrency(importe); subtotal += importe; }); const iva = subtotal * IVA_RATE; const total = subtotal + iva; subtotalEl.textContent = formatCurrency(subtotal); ivaTotalEl.textContent = formatCurrency(iva); totalEl.textContent = formatCurrency(total); }; /** * Agrega una nueva fila de concepto a la tabla. * @param {string} desc - Descripción del producto. * @param {string} satKey - Clave SAT del producto. * @param {number} qty - Cantidad. * @param {string} unit - Unidad de medida (e.g., 'H87' para Pieza). * @param {number} price - Precio unitario. */ const addItemRow = (desc = '', satKey = '', qty = 1, unit = 'H87', price = 0) => { const row = document.createElement('tr'); row.className = 'item-row'; row.innerHTML = ` `; itemRows.appendChild(row); // Añadir listeners a los nuevos inputs para recalcular al cambiar row.querySelector('.quantity').addEventListener('input', calculateTotals); row.querySelector('.unit-price').addEventListener('input', calculateTotals); // Listener para el botón de eliminar row.querySelector('.remove-item-btn').addEventListener('click', () => { row.remove(); calculateTotals(); }); }; /** * Recopila todos los datos del formulario y los conceptos en un objeto estructurado. * @returns {object} Un objeto con toda la información de la factura. */ const buildDataObject = () => { const conceptos = []; itemRows.querySelectorAll('.item-row').forEach(row => { const quantity = parseFloat(row.querySelector('.quantity').value) || 0; const unitPrice = parseFloat(row.querySelector('.unit-price').value) || 0; conceptos.push({ descripcion: row.querySelector('.description').value, claveProdServ: row.querySelector('.sat-key').value, cantidad: quantity, claveUnidad: row.querySelector('.unit').value, valorUnitario: unitPrice, importe: quantity * unitPrice }); }); const subtotal = conceptos.reduce((sum, item) => sum + item.importe, 0); const iva = subtotal * IVA_RATE; const total = subtotal + iva; return { folio: folioEl.value, fecha: new Date(fechaEl.value).toISOString(), formaPago: paymentFormEl.value, metodoPago: paymentMethodEl.value, lugarExpedicion: '76000', // Código Postal de Querétaro (ejemplo) exportacion: exportKeyEl.value, emisor: emisorData, receptor: { nombre: nombreClienteEl.value, rfc: rfcClienteEl.value, usoCFDI: usoCfdiEl.value.split(' - ')[0] // Tomar solo el código, ej: G03 }, conceptos: conceptos, subtotal: subtotal, iva: iva, total: total, }; }; /** * Resetea el formulario a su estado inicial. */ const resetForm = () => { folioEl.value = `FAC-${Math.floor(1000 + Math.random() * 9000)}`; const now = new Date(); now.setMinutes(now.getMinutes() - now.getTimezoneOffset()); fechaEl.value = now.toISOString().slice(0, 16); nombreClienteEl.value = 'Público en General'; rfcClienteEl.value = 'XAXX010101000'; usoCfdiEl.value = 'G03 - Gastos en general'; itemRows.innerHTML = ''; xmlOutputEl.textContent = 'Presiona "Timbrar y Descargar" para generar el XML...'; addItemRow('Producto de Ejemplo', '01010101', 1, 'H87', 150.00); calculateTotals(); }; // --- 5. GENERADORES DE ARCHIVOS --- /** * Construye una cadena de texto con el XML de un CFDI 4.0 simulado. * @param {object} data - El objeto de datos de la factura. * @returns {string} El XML formateado. */ const buildXml = (data) => { const conceptosXml = data.conceptos.map(c => ` `).join(''); return ` ${conceptosXml} `; }; /** * Genera un archivo PDF de la factura utilizando jsPDF y jsPDF-AutoTable. * @param {object} data - El objeto de datos de la factura. */ const generatePdf = (data) => { const { jsPDF } = window.jspdf; const doc = new jsPDF(); // --- Encabezado del PDF --- doc.setFontSize(20); doc.setFont(undefined, 'bold'); doc.text('FACTURA', 14, 22); doc.setFontSize(10); doc.setFont(undefined, 'bold'); doc.text(data.emisor.nombre, 200, 16, { align: 'right' }); doc.setFont(undefined, 'normal'); doc.text(`RFC: ${data.emisor.rfc}`, 200, 22, { align: 'right' }); doc.text(data.emisor.direccion, 200, 28, { align: 'right' }); doc.setFontSize(12); doc.text(`Folio: ${data.folio}`, 14, 30); doc.text(`Fecha: ${new Date(data.fecha).toLocaleString('es-MX')}`, 14, 36); // --- Datos del Cliente --- doc.setDrawColor(200); doc.line(14, 42, 200, 42); // Línea divisoria doc.setFontSize(10); doc.setFont(undefined, 'bold'); doc.text('Receptor:', 14, 48); doc.setFont(undefined, 'normal'); doc.text(data.receptor.nombre, 14, 54); doc.text(`RFC: ${data.receptor.rfc}`, 14, 60); doc.text(`Uso CFDI: ${data.receptor.usoCFDI}`, 14, 66); doc.line(14, 72, 200, 72); // --- Tabla de Conceptos --- const tableColumn = ["Cant.", "Unidad", "Clave SAT", "Descripción", "P. Unitario", "Importe"]; const tableRows = []; data.conceptos.forEach(item => { const itemData = [ item.cantidad, item.claveUnidad, item.claveProdServ, item.descripcion, formatCurrency(item.valorUnitario), formatCurrency(item.importe) ]; tableRows.push(itemData); }); doc.autoTable({ head: [tableColumn], body: tableRows, startY: 78, headStyles: { fillColor: [22, 160, 133], fontSize: 9 }, styles: { fontSize: 8, cellPadding: 2 }, columnStyles: { 0: { halign: 'center' }, 1: { halign: 'center' }, 2: { halign: 'center' }, 4: { halign: 'right' }, 5: { halign: 'right' } } }); // --- Totales --- const finalY = doc.autoTable.previous.finalY; const totalX = 150; doc.setFontSize(10); doc.text('Subtotal:', totalX, finalY + 10, { align: 'right' }); doc.text(formatCurrency(data.subtotal), 200, finalY + 10, { align: 'right' }); doc.text('IVA (16%):', totalX, finalY + 16, { align: 'right' }); doc.text(formatCurrency(data.iva), 200, finalY + 16, { align: 'right' }); doc.setFont(undefined, 'bold'); doc.text('Total:', totalX, finalY + 22, { align: 'right' }); doc.text(formatCurrency(data.total), 200, finalY + 22, { align: 'right' }); // --- Pie de página simulado --- const pageHeight = doc.internal.pageSize.height; doc.setFontSize(8); doc.setTextColor(150); doc.text('Este es un Comprobante Fiscal Digital por Internet (CFDI) simulado.', 14, pageHeight - 20); doc.text(`UUID Timbre: ${generateUUID()}`, 14, pageHeight - 15); // --- Guardar PDF --- doc.save(`Factura-${data.folio}.pdf`); }; // --- 6. EVENT LISTENERS --- addItemBtn.addEventListener('click', () => addItemRow()); resetBtn.addEventListener('click', resetForm); stampBtn.addEventListener('click', () => { calculateTotals(); const data = buildDataObject(); const xml = buildXml(data); // Resaltar sintaxis del XML xmlOutputEl.innerHTML = xml.replace(//g, '>'); generatePdf(data); }); // --- 7. INICIALIZACIÓN --- const init = () => { // Cargar datos del emisor en la UI nombreEmisorEl.textContent = emisorData.nombre; rfcEmisorEl.textContent = `RFC: ${emisorData.rfc}`; direccionEmisorEl.textContent = emisorData.direccion; // Iniciar formulario con valores por defecto resetForm(); }; init(); // Ejecutar la función de inicialización al cargar la página. });