# 9. Generación de PDF ## Descripción El sistema genera reportes PDF profesionales de **32 páginas** usando Browsershot (wrapper de Puppeteer para Laravel). --- ## Arquitectura ``` ┌─────────────────┐ │ ReporteController│ │ /pdf │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ GeneradorPdf │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Browsershot │ ← Puppeteer headless Chrome └────────┬────────┘ │ ▼ ┌─────────────────┐ │ React PdfView │ ← Componente optimizado para impresión └────────┬────────┘ │ ▼ ┌─────────────────┐ │ PDF 32 págs │ └─────────────────┘ ``` --- ## Estructura del PDF (32 páginas) | # | Sección | Contenido | |---|---------|-----------| | 1 | Portada | Logo, nombre empresa, periodo | | 2 | Índice | Tabla de contenidos | | 3-4 | Resumen Ejecutivo | KPIs principales, semáforos | | 5-8 | Balance General | Activos, Pasivos, Capital | | 9-12 | Estado de Resultados | Ingresos, Costos, Gastos | | 13-14 | Flujo de Efectivo | Operación, Inversión, Financiamiento | | 15-18 | Análisis de Márgenes | 7 métricas con gráficas | | 19-20 | Análisis de Retorno | ROIC, ROE, ROA, ROCE | | 21-22 | Análisis de Eficiencia | Rotaciones, ciclo conversión | | 23-24 | Análisis de Liquidez | Ratios de liquidez | | 25-26 | Análisis de Solvencia | Deuda, cobertura | | 27-28 | Análisis de Gestión | Crecimiento, inversión | | 29-30 | Comparativos | Tendencias históricas | | 31 | Notas | Observaciones y anomalías | | 32 | Glosario | Definiciones de métricas | --- ## Implementación Backend ### GeneradorPdf.php ```php id; // Nombre del archivo $filename = sprintf( 'reportes/%s/%s-%s.pdf', $reporte->cliente_id, $reporte->periodo_fin->format('Y-m'), now()->timestamp ); $fullPath = storage_path('app/public/' . $filename); // Asegurar directorio existe $dir = dirname($fullPath); if (!is_dir($dir)) { mkdir($dir, 0755, true); } // Generar PDF con Browsershot Browsershot::url($url) ->setNodeBinary(config('browsershot.node_binary')) ->setNpmBinary(config('browsershot.npm_binary')) ->setChromePath(config('browsershot.chrome_path')) ->format('Letter') ->margins(10, 10, 10, 10) ->showBackground() ->waitUntilNetworkIdle() ->timeout(120) ->save($fullPath); // Actualizar reporte $reporte->update([ 'pdf_path' => $filename, 'status' => 'completado', ]); return $filename; } } ``` ### Configuración Browsershot ```php // config/browsershot.php return [ 'node_binary' => env('NODE_BINARY', '/usr/bin/node'), 'npm_binary' => env('NPM_BINARY', '/usr/bin/npm'), 'chrome_path' => env('CHROME_PATH', null), // null = usa Chromium de Puppeteer ]; ``` --- ## Implementación Frontend ### PdfView Component ```tsx // src/pages/PdfView/index.tsx import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { api } from '@/services/api'; import { Reporte } from '@/types'; // Importar secciones import Portada from './sections/Portada'; import Indice from './sections/Indice'; import ResumenEjecutivo from './sections/ResumenEjecutivo'; import BalanceGeneral from './sections/BalanceGeneral'; import EstadoResultados from './sections/EstadoResultados'; import FlujoEfectivo from './sections/FlujoEfectivo'; import AnalisisMargenes from './sections/AnalisisMargenes'; import AnalisisRetorno from './sections/AnalisisRetorno'; import AnalisisEficiencia from './sections/AnalisisEficiencia'; import AnalisisLiquidez from './sections/AnalisisLiquidez'; import AnalisisSolvencia from './sections/AnalisisSolvencia'; import AnalisisGestion from './sections/AnalisisGestion'; import Comparativos from './sections/Comparativos'; import Notas from './sections/Notas'; import Glosario from './sections/Glosario'; export default function PdfView() { const { id } = useParams<{ id: string }>(); const [reporte, setReporte] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const fetchReporte = async () => { try { const data = await api.reportes.get(Number(id)); setReporte(data); } finally { setLoading(false); } }; fetchReporte(); }, [id]); if (loading || !reporte) { return
Cargando...
; } return (
); } ``` ### Estilos de Impresión ```css /* src/styles/pdf.css */ @media print { /* Ocultar elementos no imprimibles */ .no-print { display: none !important; } /* Saltos de página */ .page-break { page-break-after: always; } .page-break-before { page-break-before: always; } .avoid-break { page-break-inside: avoid; } } /* Contenedor PDF */ .pdf-container { width: 8.5in; margin: 0 auto; background: white; font-family: 'Inter', sans-serif; } /* Página individual */ .pdf-page { width: 8.5in; min-height: 11in; padding: 0.5in; box-sizing: border-box; position: relative; } /* Header de página */ .pdf-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 0.25in; border-bottom: 2px solid #1A1F36; margin-bottom: 0.25in; } /* Footer de página */ .pdf-footer { position: absolute; bottom: 0.25in; left: 0.5in; right: 0.5in; display: flex; justify-content: space-between; font-size: 10px; color: #666; } /* Tabla de datos */ .pdf-table { width: 100%; border-collapse: collapse; font-size: 11px; } .pdf-table th, .pdf-table td { padding: 6px 8px; text-align: left; border-bottom: 1px solid #e5e7eb; } .pdf-table th { background: #f3f4f6; font-weight: 600; } /* Semáforos */ .indicator { display: inline-block; width: 12px; height: 12px; border-radius: 50%; } .indicator-muy_positivo { background: #059669; } .indicator-positivo { background: #10b981; } .indicator-neutral { background: #f59e0b; } .indicator-negativo { background: #f97316; } .indicator-muy_negativo { background: #ef4444; } ``` --- ## Componentes de Sección ### Portada ```tsx // src/pages/PdfView/sections/Portada.tsx interface PortadaProps { reporte: Reporte; } export default function Portada({ reporte }: PortadaProps) { return (
{/* Logo */} Logo {/* Nombre empresa */}

{reporte.cliente.nombre_empresa}

{/* Tipo de reporte */}

Reporte Financiero

{/* Periodo */}

{formatPeriodo(reporte.periodo_tipo, reporte.periodo_inicio, reporte.periodo_fin)}

{/* Fecha generación */}

Generado: {formatDate(reporte.fecha_generacion)}

{/* Branding */}

Powered by Horux 360

); } ``` ### Resumen Ejecutivo ```tsx // src/pages/PdfView/sections/ResumenEjecutivo.tsx export default function ResumenEjecutivo({ reporte }: { reporte: Reporte }) { const { metricas, comparativos } = reporte.data_calculada; const kpis = [ { nombre: 'Ingresos', valor: metricas.ingresos, formato: 'currency' }, { nombre: 'Margen EBITDA', valor: metricas.margen_ebitda, formato: 'percent' }, { nombre: 'Margen Neto', valor: metricas.margen_neto, formato: 'percent' }, { nombre: 'ROIC', valor: metricas.roic, formato: 'percent' }, { nombre: 'Current Ratio', valor: metricas.current_ratio, formato: 'number' }, { nombre: 'Net Debt/EBITDA', valor: metricas.net_debt_ebitda, formato: 'number' }, ]; return ( <>
{kpis.map((kpi) => ( ))}

Semáforos de Rendimiento

Tendencias Principales

); } ``` --- ## Requisitos del Servidor ### Instalación de Dependencias ```bash # Node.js 18+ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt-get install -y nodejs # Puppeteer dependencies (Ubuntu/Debian) sudo apt-get install -y \ libnss3 \ libatk1.0-0 \ libatk-bridge2.0-0 \ libcups2 \ libdrm2 \ libxkbcommon0 \ libxcomposite1 \ libxdamage1 \ libxfixes3 \ libxrandr2 \ libgbm1 \ libasound2 # Instalar Puppeteer cd backend npm install puppeteer ``` ### Configuración en Producción ```env # .env NODE_BINARY=/usr/bin/node NPM_BINARY=/usr/bin/npm CHROME_PATH=/usr/bin/google-chrome-stable FRONTEND_URL=https://app.horux360.com ``` --- ## Troubleshooting ### Error: "Unable to launch browser" ```bash # Verificar Chrome instalado which google-chrome-stable # Instalar Chrome si no existe wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb sudo dpkg -i google-chrome-stable_current_amd64.deb sudo apt-get -f install ``` ### Error: "Timeout exceeded" Aumentar timeout en `GeneradorPdf.php`: ```php Browsershot::url($url) ->timeout(300) // 5 minutos ->waitUntilNetworkIdle(false) // No esperar red ->save($fullPath); ``` ### Error: "Missing fonts" ```bash # Instalar fuentes sudo apt-get install -y fonts-liberation fonts-noto-color-emoji ```