UNPKG

invoice-processor-mcp

Version:
289 lines (255 loc) 10.2 kB
// invoiceProcessor.js - Lógica para procesar facturas en Excel import * as XLSX from 'xlsx'; import ExcelJS from 'exceljs'; import { promises as fs } from 'fs'; import path from 'path'; /** * Procesa un archivo de factura en Excel y extrae información relevante * @param {string} filePath Ruta al archivo Excel * @returns {Promise<object>} Información estructurada de la factura */ export async function processInvoice(filePath) { try { // Verificar que el archivo existe await fs.access(filePath); // Leer el archivo Excel const workbook = XLSX.readFile(filePath); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; // Convertir a JSON para procesamiento const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); // Estructura básica para una factura const invoiceData = { invoiceNumber: '', date: '', customer: { name: '', address: '', taxId: '' }, items: [], totals: { subtotal: 0, tax: 0, total: 0 }, metadata: { processedAt: new Date().toISOString(), source: path.basename(filePath) } }; // Extraer información de la factura // Este es un ejemplo simple; en un caso real necesitarías adaptar // esto según la estructura de tus facturas // Buscar cabeceras y datos principales for (let i = 0; i < jsonData.length; i++) { const row = jsonData[i]; // Buscar el número de factura (normalmente en las primeras filas) if (row && row.join(' ').toLowerCase().includes('factura')) { // Extracción simple de un posible número de factura for (let j = 0; j < row.length; j++) { if (typeof row[j] === 'string' && row[j].match(/\d+/)) { invoiceData.invoiceNumber = row[j].match(/\d+/)[0]; break; } } } // Buscar la fecha (normalmente en las primeras filas) if (row && row.join(' ').toLowerCase().includes('fecha')) { // Extracción simple de una fecha for (let j = 0; j < row.length; j++) { if (row[j] instanceof Date) { invoiceData.date = row[j].toISOString().split('T')[0]; break; } else if (typeof row[j] === 'string' && row[j].match(/\d{1,2}\/\d{1,2}\/\d{2,4}/)) { invoiceData.date = row[j]; break; } } } // Buscar información del cliente if (row && row.join(' ').toLowerCase().includes('cliente')) { if (i + 1 < jsonData.length && jsonData[i + 1][0]) { invoiceData.customer.name = jsonData[i + 1][0]; } if (i + 2 < jsonData.length && jsonData[i + 2][0]) { invoiceData.customer.address = jsonData[i + 2][0]; } } // Buscar tabla de items if (row && row.join('').toLowerCase().includes('descripción') && row.join('').toLowerCase().includes('cantidad') && row.join('').toLowerCase().includes('precio')) { // Índices de columnas let descIdx = -1, qtyIdx = -1, priceIdx = -1, totalIdx = -1; for (let j = 0; j < row.length; j++) { if (typeof row[j] === 'string') { const cell = row[j].toLowerCase(); if (cell.includes('descrip')) descIdx = j; if (cell.includes('cant')) qtyIdx = j; if (cell.includes('prec')) priceIdx = j; if (cell.includes('total') || cell.includes('importe')) totalIdx = j; } } // Extraer items for (let r = i + 1; r < jsonData.length; r++) { const itemRow = jsonData[r]; // Verificar si ya llegamos al final de la tabla de items if (!itemRow || itemRow.length === 0 || (itemRow.join('').toLowerCase().includes('subtotal') || itemRow.join('').toLowerCase().includes('total'))) { break; } // Verificar si la fila tiene datos suficientes if (itemRow.length > priceIdx) { const item = { description: descIdx >= 0 ? itemRow[descIdx] || '' : '', quantity: qtyIdx >= 0 ? parseFloat(itemRow[qtyIdx]) || 0 : 0, unitPrice: priceIdx >= 0 ? parseFloat(itemRow[priceIdx]) || 0 : 0, total: totalIdx >= 0 ? parseFloat(itemRow[totalIdx]) || 0 : 0 }; // Calcular el total si no está disponible if (!item.total && item.quantity && item.unitPrice) { item.total = item.quantity * item.unitPrice; } // Solo agregar items que parecen válidos if (item.description && (item.quantity || item.unitPrice || item.total)) { invoiceData.items.push(item); } } } } // Buscar totales if (row && row[0] && typeof row[0] === 'string') { const firstCell = row[0].toLowerCase(); if (firstCell.includes('subtotal')) { for (let j = 1; j < row.length; j++) { if (typeof row[j] === 'number') { invoiceData.totals.subtotal = row[j]; break; } else if (typeof row[j] === 'string' && row[j].match(/[\d.,]+/)) { invoiceData.totals.subtotal = parseFloat(row[j].replace(/[^\d.,]/g, '').replace(',', '.')); break; } } } if (firstCell.includes('iva') || firstCell.includes('tax') || firstCell.includes('impuesto')) { for (let j = 1; j < row.length; j++) { if (typeof row[j] === 'number') { invoiceData.totals.tax = row[j]; break; } else if (typeof row[j] === 'string' && row[j].match(/[\d.,]+/)) { invoiceData.totals.tax = parseFloat(row[j].replace(/[^\d.,]/g, '').replace(',', '.')); break; } } } if (firstCell.includes('total')) { for (let j = 1; j < row.length; j++) { if (typeof row[j] === 'number') { invoiceData.totals.total = row[j]; break; } else if (typeof row[j] === 'string' && row[j].match(/[\d.,]+/)) { invoiceData.totals.total = parseFloat(row[j].replace(/[^\d.,]/g, '').replace(',', '.')); break; } } } } } // Si no se encontró el total, calcularlo if (!invoiceData.totals.total) { if (invoiceData.items.length > 0) { invoiceData.totals.subtotal = invoiceData.items.reduce((sum, item) => sum + item.total, 0); // Asumiendo un 21% de IVA estándar si no se encontró if (!invoiceData.totals.tax) { invoiceData.totals.tax = invoiceData.totals.subtotal * 0.21; } invoiceData.totals.total = invoiceData.totals.subtotal + invoiceData.totals.tax; } } return invoiceData; } catch (error) { console.error(`Error procesando la factura ${filePath}:`, error); throw new Error(`No se pudo procesar la factura: ${error.message}`); } } /** * Genera un archivo de resumen de múltiples facturas * @param {Array<object>} invoicesData Array de datos de facturas procesadas * @param {string} outputPath Ruta de salida para el archivo Excel de resumen * @returns {Promise<void>} */ export async function generateInvoiceSummary(invoicesData, outputPath) { try { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('Resumen Facturas'); // Definir columnas worksheet.columns = [ { header: 'Nº Factura', key: 'invoiceNumber', width: 15 }, { header: 'Fecha', key: 'date', width: 15 }, { header: 'Cliente', key: 'customerName', width: 30 }, { header: 'Subtotal', key: 'subtotal', width: 15 }, { header: 'IVA', key: 'tax', width: 15 }, { header: 'Total', key: 'total', width: 15 } ]; // Agregar filas con los datos de cada factura invoicesData.forEach(invoice => { worksheet.addRow({ invoiceNumber: invoice.invoiceNumber, date: invoice.date, customerName: invoice.customer.name, subtotal: invoice.totals.subtotal, tax: invoice.totals.tax, total: invoice.totals.total }); }); // Dar formato a las celdas de moneda worksheet.getColumn('subtotal').numFmt = '#,##0.00 €'; worksheet.getColumn('tax').numFmt = '#,##0.00 €'; worksheet.getColumn('total').numFmt = '#,##0.00 €'; // Añadir totales const rowCount = worksheet.rowCount; worksheet.addRow({}); const totalRow = worksheet.addRow({ invoiceNumber: '', date: '', customerName: 'TOTAL', subtotal: { formula: `SUM(D2:D${rowCount})` }, tax: { formula: `SUM(E2:E${rowCount})` }, total: { formula: `SUM(F2:F${rowCount})` } }); // Destacar la fila de totales totalRow.font = { bold: true }; // Guardar el workbook await workbook.xlsx.writeFile(outputPath); return outputPath; } catch (error) { console.error(`Error generando el resumen de facturas:`, error); throw new Error(`No se pudo generar el resumen: ${error.message}`); } } /** * Convierte datos de factura a formato adecuado para Cloud Functions * @param {object} invoiceData Datos de la factura procesada * @returns {object} Datos formateados para almacenamiento o API */ export function formatInvoiceForAPI(invoiceData) { // Simplificamos los datos para la API return { id: invoiceData.invoiceNumber, date: invoiceData.date, customer: invoiceData.customer.name, amount: invoiceData.totals.total, currency: 'EUR', // Asumimos EUR por defecto items: invoiceData.items.map(item => ({ description: item.description, quantity: item.quantity, unitPrice: item.unitPrice, total: item.total })), tax: invoiceData.totals.tax, processed: true, processedAt: invoiceData.metadata.processedAt }; }