Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
5.1 KiB
5.1 KiB
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
import PDFDocument from 'pdfkit';
import ExcelJS from 'exceljs';
@Injectable()
export class EstimationReportService {
async generatePDF(estimacionId: string, templateName: string): Promise<Buffer> {
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<Buffer> {
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