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
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.
});
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...