invoice-processor-mcp
Version:
MCP server para procesar facturas en formato Excel
289 lines (255 loc) • 10.2 kB
JavaScript
// 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
};
}