# ET-EST-004: Generación de Reportes PDF/Excel **ID:** ET-EST-004 **Módulo:** MAI-008 **Relacionado con:** RF-EST-004 --- ## 🔧 Backend Service ### estimation-report.service.ts ```typescript import PDFDocument from 'pdfkit'; import ExcelJS from 'exceljs'; @Injectable() export class EstimationReportService { async generatePDF(estimacionId: string, templateName: string): Promise { const estimation = await this.estimationsRepo.findOne(estimacionId, { relations: ['items', 'project', 'contract'] }); const template = await this.getTemplate(templateName); const doc = new PDFDocument({ size: 'LETTER', margin: 50 }); const chunks: Buffer[] = []; doc.on('data', chunk => chunks.push(chunk)); // Header this.addHeader(doc, estimation, template); // Resumen financiero this.addFinancialSummary(doc, estimation); // Detalle de items this.addItemsTable(doc, estimation.items); // Amortizaciones y retenciones this.addAmortizacionesRetenciones(doc, estimation); // Firmas this.addSignatures(doc, estimation); doc.end(); return new Promise((resolve) => { doc.on('end', () => resolve(Buffer.concat(chunks))); }); } async generateExcel(estimacionId: string): Promise { const estimation = await this.estimationsRepo.findOne(estimacionId, { relations: ['items'] }); const workbook = new ExcelJS.Workbook(); // Hoja 1: Resumen const resumenSheet = workbook.addWorksheet('Resumen'); this.populateResumenSheet(resumenSheet, estimation); // Hoja 2: Detalle const detalleSheet = workbook.addWorksheet('Detalle'); this.populateDetalleSheet(detalleSheet, estimation.items); // Hoja 3: Amortizaciones const amortSheet = workbook.addWorksheet('Amortizaciones'); await this.populateAmortizacionesSheet(amortSheet, estimation); return workbook.xlsx.writeBuffer(); } private addHeader(doc: PDFDocument, estimation: Estimation, template: Template): void { // Logo if (template.logo) { doc.image(template.logo, 50, 50, { width: 100 }); } doc.fontSize(18).text('ESTIMACIÓN DE OBRA', 200, 60); doc.fontSize(12).text(`No. ${estimation.numero}`, 200, 85); doc.fontSize(10).text(`Proyecto: ${estimation.project.nombre}`, 50, 120); doc.text(`Periodo: ${format(estimation.periodoInicio, 'dd/MM/yyyy')} - ${format(estimation.periodoFin, 'dd/MM/yyyy')}`, 50, 135); } private addFinancialSummary(doc: PDFDocument, estimation: Estimation): void { doc.moveDown(2); doc.fontSize(14).text('RESUMEN FINANCIERO', { underline: true }); doc.moveDown(); const formatMoney = (amount: number) => new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }) .format(amount / 100); doc.fontSize(11); doc.text(`Monto Bruto: ${formatMoney(estimation.montoBruto)}`, { indent: 20 }); doc.text(`(-) Amortización: ${formatMoney(estimation.amortizacionAnticipo)}`, { indent: 20 }); doc.text(`(-) Retenciones: ${formatMoney(estimation.totalRetenciones)}`, { indent: 20 }); doc.moveTo(70, doc.y).lineTo(300, doc.y).stroke(); doc.text(`Monto Neto: ${formatMoney(estimation.montoNeto)}`, { indent: 20 }); } private addItemsTable(doc: PDFDocument, items: EstimationItem[]): void { doc.moveDown(2); doc.fontSize(14).text('DETALLE DE CONCEPTOS', { underline: true }); doc.moveDown(); // Table headers const tableTop = doc.y; doc.fontSize(9); doc.text('No.', 50, tableTop); doc.text('Concepto', 80, tableTop); doc.text('Unidad', 300, tableTop); doc.text('Cantidad', 350, tableTop); doc.text('P.U.', 410, tableTop); doc.text('Importe', 480, tableTop); let y = tableTop + 20; items.forEach((item, index) => { doc.text(String(index + 1), 50, y); doc.text(item.descripcion.substring(0, 40), 80, y); doc.text(item.unidad, 300, y); doc.text(String(item.cantidadEstimadaActual), 350, y); doc.text(formatMoney(item.precioUnitario), 410, y); doc.text(formatMoney(item.importeActual), 480, y); y += 20; }); } private populateDetalleSheet(sheet: ExcelJS.Worksheet, items: EstimationItem[]): void { sheet.columns = [ { header: 'No.', key: 'numero', width: 5 }, { header: 'Concepto', key: 'concepto', width: 50 }, { header: 'Unidad', key: 'unidad', width: 10 }, { header: 'Cantidad', key: 'cantidad', width: 12 }, { header: 'P.U.', key: 'precioUnitario', width: 15 }, { header: 'Importe', key: 'importe', width: 15 } ]; items.forEach((item, index) => { sheet.addRow({ numero: index + 1, concepto: item.descripcion, unidad: item.unidad, cantidad: item.cantidadEstimadaActual, precioUnitario: item.precioUnitario / 100, importe: { formula: `D${sheet.rowCount + 1}*E${sheet.rowCount + 1}` } }); }); // Formato sheet.getRow(1).font = { bold: true }; sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } }; } } ``` --- **Generado:** 2025-11-20 **Estado:** ✅ Completo