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>
This commit is contained in:
479
docs/09-pdf.md
Normal file
479
docs/09-pdf.md
Normal file
@@ -0,0 +1,479 @@
|
||||
# 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
|
||||
<?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
|
||||
|
||||
```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<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
|
||||
|
||||
```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 (
|
||||
<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
|
||||
|
||||
```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 (
|
||||
<>
|
||||
<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
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user