Files
horux-strategy-platform/docs/09-pdf.md
Torch2196 4c3dc94ff2 Initial commit: Horux Strategy Platform
- Laravel 11 backend with API REST
- React 18 + TypeScript + Vite frontend
- Multi-parser architecture for accounting systems (CONTPAQi, Aspel, SAP)
- 27+ financial metrics calculation
- PDF report generation with Browsershot
- Complete documentation (10 documents)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 22:24:00 -06:00

12 KiB

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

namespace App\Services;

use App\Models\Reporte;
use Spatie\Browsershot\Browsershot;
use Illuminate\Support\Facades\Storage;

class GeneradorPdf
{
    public function generar(Reporte $reporte): string
    {
        // URL del frontend con datos del reporte
        $url = config('app.frontend_url') . '/pdf/' . $reporte->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

// 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

// 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<Reporte | null>(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 <div>Cargando...</div>;
  }

  return (
    <div className="pdf-container">
      <Portada reporte={reporte} />
      <Indice />
      <ResumenEjecutivo reporte={reporte} />
      <BalanceGeneral reporte={reporte} />
      <EstadoResultados reporte={reporte} />
      <FlujoEfectivo reporte={reporte} />
      <AnalisisMargenes reporte={reporte} />
      <AnalisisRetorno reporte={reporte} />
      <AnalisisEficiencia reporte={reporte} />
      <AnalisisLiquidez reporte={reporte} />
      <AnalisisSolvencia reporte={reporte} />
      <AnalisisGestion reporte={reporte} />
      <Comparativos reporte={reporte} />
      <Notas reporte={reporte} />
      <Glosario />
    </div>
  );
}

Estilos de Impresión

/* 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

// src/pages/PdfView/sections/Portada.tsx

interface PortadaProps {
  reporte: Reporte;
}

export default function Portada({ reporte }: PortadaProps) {
  return (
    <div className="pdf-page flex flex-col items-center justify-center">
      {/* Logo */}
      <img
        src={reporte.cliente.logo || '/logo-horux.png'}
        alt="Logo"
        className="w-48 mb-8"
      />

      {/* Nombre empresa */}
      <h1 className="text-4xl font-bold text-horux-dark mb-4">
        {reporte.cliente.nombre_empresa}
      </h1>

      {/* Tipo de reporte */}
      <h2 className="text-2xl text-gray-600 mb-8">
        Reporte Financiero
      </h2>

      {/* Periodo */}
      <p className="text-xl text-gray-500">
        {formatPeriodo(reporte.periodo_tipo, reporte.periodo_inicio, reporte.periodo_fin)}
      </p>

      {/* Fecha generación */}
      <p className="text-sm text-gray-400 mt-4">
        Generado: {formatDate(reporte.fecha_generacion)}
      </p>

      {/* Branding */}
      <div className="absolute bottom-8">
        <p className="text-sm text-gray-400">
          Powered by Horux 360
        </p>
      </div>
    </div>
  );
}

Resumen Ejecutivo

// 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 (
    <>
      <div className="pdf-page page-break">
        <PdfHeader titulo="Resumen Ejecutivo" pagina={3} />

        <div className="grid grid-cols-3 gap-4 mb-8">
          {kpis.map((kpi) => (
            <KpiCard key={kpi.nombre} {...kpi} />
          ))}
        </div>

        <h3 className="text-lg font-bold mb-4">Semáforos de Rendimiento</h3>
        <SemaforoTable metricas={metricas} />

        <PdfFooter pagina={3} />
      </div>

      <div className="pdf-page page-break">
        <PdfHeader titulo="Resumen Ejecutivo" pagina={4} />

        <h3 className="text-lg font-bold mb-4">Tendencias Principales</h3>
        <TrendCharts comparativos={comparativos} />

        <PdfFooter pagina={4} />
      </div>
    </>
  );
}

Requisitos del Servidor

Instalación de Dependencias

# 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
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"

# 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:

Browsershot::url($url)
    ->timeout(300) // 5 minutos
    ->waitUntilNetworkIdle(false) // No esperar red
    ->save($fullPath);

Error: "Missing fonts"

# Instalar fuentes
sudo apt-get install -y fonts-liberation fonts-noto-color-emoji