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:
2026-01-31 22:24:00 -06:00
commit 4c3dc94ff2
107 changed files with 10701 additions and 0 deletions

View File

@@ -0,0 +1,419 @@
<?php
namespace App\Services;
use App\Models\Reporte;
use App\Models\Umbral;
use App\Models\CategoriaContable;
class CalculadorMetricas
{
private array $categorias;
private ?int $giroId;
public function calcular(Reporte $reporte): array
{
$this->giroId = $reporte->cliente->giro_id;
$this->cargarCategorias();
$balanzas = $reporte->balanzas()->with('cuentas')->orderBy('periodo_fin')->get();
// Calcular estados financieros para cada periodo
$periodos = [];
foreach ($balanzas as $balanza) {
$periodos[] = [
'periodo' => $balanza->periodo_fin->format('Y-m'),
'balance_general' => $this->calcularBalanceGeneral($balanza),
'estado_resultados' => $this->calcularEstadoResultados($balanza),
];
}
// Calcular métricas del último periodo
$ultimoPeriodo = end($periodos);
$metricas = $this->calcularTodasLasMetricas($ultimoPeriodo);
// Calcular comparativos si hay múltiples periodos
$comparativos = [];
if (count($periodos) >= 2) {
$comparativos = $this->calcularComparativos($periodos, $metricas);
}
// Calcular flujo de efectivo
$flujoEfectivo = $this->calcularFlujoEfectivo($periodos);
return [
'periodos' => $periodos,
'metricas' => $metricas,
'comparativos' => $comparativos,
'flujo_efectivo' => $flujoEfectivo,
'estados_financieros' => [
'balance_general' => $ultimoPeriodo['balance_general'],
'estado_resultados' => $ultimoPeriodo['estado_resultados'],
'flujo_efectivo' => $flujoEfectivo,
],
];
}
private function cargarCategorias(): void
{
$this->categorias = CategoriaContable::all()->keyBy('nombre')->toArray();
}
private function calcularBalanceGeneral($balanza): array
{
$cuentas = $balanza->cuentasActivas()
->whereHas('reporteContable', fn($q) => $q->where('nombre', 'Balance General'))
->get();
$totales = [
'activos_circulantes' => 0,
'activos_no_circulantes' => 0,
'total_activos' => 0,
'pasivo_circulante' => 0,
'pasivo_no_circulante' => 0,
'total_pasivos' => 0,
'capital_social' => 0,
'utilidades_anteriores' => 0,
'perdidas_anteriores' => 0,
'total_capital' => 0,
];
foreach ($cuentas as $cuenta) {
if (!$cuenta->categoriaContable) continue;
$saldo = $cuenta->saldo_final_neto;
$categoria = $cuenta->categoriaContable->nombre;
switch ($categoria) {
case 'Activos Circulantes':
$totales['activos_circulantes'] += $saldo;
break;
case 'Activos No Circulantes':
$totales['activos_no_circulantes'] += $saldo;
break;
case 'Pasivo Circulante':
$totales['pasivo_circulante'] += abs($saldo);
break;
case 'Pasivo No Circulante':
$totales['pasivo_no_circulante'] += abs($saldo);
break;
case 'Capital Social':
$totales['capital_social'] += abs($saldo);
break;
case 'Utilidades Ejercicios Anteriores':
$totales['utilidades_anteriores'] += abs($saldo);
break;
case 'Pérdidas Ejercicios Anteriores':
$totales['perdidas_anteriores'] += abs($saldo);
break;
}
}
$totales['total_activos'] = $totales['activos_circulantes'] + $totales['activos_no_circulantes'];
$totales['total_pasivos'] = $totales['pasivo_circulante'] + $totales['pasivo_no_circulante'];
$totales['total_capital'] = $totales['capital_social'] + $totales['utilidades_anteriores'] - $totales['perdidas_anteriores'];
return $totales;
}
private function calcularEstadoResultados($balanza): array
{
$cuentas = $balanza->cuentasActivas()
->whereHas('reporteContable', fn($q) => $q->where('nombre', 'Estado de Resultados'))
->get();
$totales = [
'ingresos' => 0,
'costo_venta' => 0,
'utilidad_bruta' => 0,
'gastos_operativos' => 0,
'utilidad_operativa' => 0,
'otros_gastos' => 0,
'gastos_financieros' => 0,
'utilidad_antes_impuestos' => 0,
'impuestos' => 0,
'utilidad_neta' => 0,
];
foreach ($cuentas as $cuenta) {
if (!$cuenta->categoriaContable) continue;
$saldo = abs($cuenta->saldo_final_neto);
$categoria = $cuenta->categoriaContable->nombre;
switch ($categoria) {
case 'Ingresos':
$totales['ingresos'] += $saldo;
break;
case 'Costo de Venta':
$totales['costo_venta'] += $saldo;
break;
case 'Gastos Operativos':
$totales['gastos_operativos'] += $saldo;
break;
case 'Otros Gastos':
$totales['otros_gastos'] += $saldo;
break;
case 'Gastos Financieros':
$totales['gastos_financieros'] += $saldo;
break;
}
}
// Calcular subtotales
$totales['utilidad_bruta'] = $totales['ingresos'] - $totales['costo_venta'];
$totales['utilidad_operativa'] = $totales['utilidad_bruta'] - $totales['gastos_operativos'];
$totales['utilidad_antes_impuestos'] = $totales['utilidad_operativa'] - $totales['otros_gastos'] - $totales['gastos_financieros'];
// Estimación de impuestos (30% para México)
$totales['impuestos'] = max(0, $totales['utilidad_antes_impuestos'] * 0.30);
$totales['utilidad_neta'] = $totales['utilidad_antes_impuestos'] - $totales['impuestos'];
return $totales;
}
private function calcularTodasLasMetricas(array $periodo): array
{
$balance = $periodo['balance_general'];
$resultados = $periodo['estado_resultados'];
$metricas = [];
// === MÁRGENES ===
$ingresos = $resultados['ingresos'] ?: 1; // Evitar división por cero
$metricas['margen_bruto'] = $this->crearMetrica(
'Margen Bruto',
$resultados['utilidad_bruta'] / $ingresos,
'margen_bruto'
);
// EBITDA = Utilidad Operativa + Depreciación (estimada como 5% de activos fijos)
$depreciacion = $balance['activos_no_circulantes'] * 0.05;
$ebitda = $resultados['utilidad_operativa'] + $depreciacion;
$metricas['margen_ebitda'] = $this->crearMetrica(
'Margen EBITDA',
$ebitda / $ingresos,
'margen_ebitda'
);
$metricas['margen_operativo'] = $this->crearMetrica(
'Margen Operativo',
$resultados['utilidad_operativa'] / $ingresos,
'margen_operativo'
);
$metricas['margen_neto'] = $this->crearMetrica(
'Margen Neto',
$resultados['utilidad_neta'] / $ingresos,
'margen_neto'
);
// NOPAT = EBIT * (1 - Tasa impuestos)
$nopat = $resultados['utilidad_operativa'] * 0.70;
$metricas['margen_nopat'] = $this->crearMetrica(
'Margen NOPAT',
$nopat / $ingresos,
'margen_nopat'
);
// === RETORNO ===
$capitalInvertido = $balance['total_activos'] - $balance['pasivo_circulante'];
$capitalEmpleado = $balance['total_activos'] - $balance['pasivo_circulante'];
$metricas['roic'] = $this->crearMetrica(
'ROIC',
$capitalInvertido > 0 ? $nopat / $capitalInvertido : 0,
'roic'
);
$metricas['roe'] = $this->crearMetrica(
'ROE',
$balance['total_capital'] > 0 ? $resultados['utilidad_neta'] / $balance['total_capital'] : 0,
'roe'
);
$metricas['roa'] = $this->crearMetrica(
'ROA',
$balance['total_activos'] > 0 ? $resultados['utilidad_neta'] / $balance['total_activos'] : 0,
'roa'
);
$metricas['roce'] = $this->crearMetrica(
'ROCE',
$capitalEmpleado > 0 ? $resultados['utilidad_operativa'] / $capitalEmpleado : 0,
'roce'
);
// === LIQUIDEZ ===
$pasivoCirculante = $balance['pasivo_circulante'] ?: 1;
$metricas['current_ratio'] = $this->crearMetrica(
'Current Ratio',
$balance['activos_circulantes'] / $pasivoCirculante,
'current_ratio'
);
// Quick ratio (estimando inventario como 30% de activos circulantes)
$inventarioEstimado = $balance['activos_circulantes'] * 0.30;
$metricas['quick_ratio'] = $this->crearMetrica(
'Quick Ratio',
($balance['activos_circulantes'] - $inventarioEstimado) / $pasivoCirculante,
'quick_ratio'
);
// Cash ratio (estimando efectivo como 15% de activos circulantes)
$efectivoEstimado = $balance['activos_circulantes'] * 0.15;
$metricas['cash_ratio'] = $this->crearMetrica(
'Cash Ratio',
$efectivoEstimado / $pasivoCirculante,
'cash_ratio'
);
// === SOLVENCIA ===
$deudaTotal = $balance['total_pasivos'];
$ebitdaAnual = $ebitda * 12; // Anualizar si es mensual
$metricas['net_debt_ebitda'] = $this->crearMetrica(
'Net Debt / EBITDA',
$ebitdaAnual > 0 ? ($deudaTotal - $efectivoEstimado) / $ebitdaAnual : 0,
'net_debt_ebitda'
);
$gastosFinancieros = $resultados['gastos_financieros'] ?: 1;
$metricas['interest_coverage'] = $this->crearMetrica(
'Interest Coverage',
$ebitda / $gastosFinancieros,
'interest_coverage'
);
$metricas['debt_ratio'] = $this->crearMetrica(
'Debt Ratio',
$balance['total_activos'] > 0 ? $deudaTotal / $balance['total_activos'] : 0,
'debt_ratio'
);
return $metricas;
}
private function crearMetrica(string $nombre, float $valor, string $codigoUmbral): array
{
$umbral = $this->obtenerUmbral($codigoUmbral);
$tendencia = $umbral ? $umbral->evaluarValor($valor) : 'neutral';
return [
'nombre' => $nombre,
'valor' => round($valor, 4),
'valor_porcentaje' => round($valor * 100, 2),
'tendencia' => $tendencia,
];
}
private function obtenerUmbral(string $metrica): ?Umbral
{
// Buscar umbral específico del giro primero
$umbral = Umbral::where('metrica', $metrica)
->where('giro_id', $this->giroId)
->first();
if (!$umbral) {
// Buscar umbral genérico
$umbral = Umbral::where('metrica', $metrica)
->whereNull('giro_id')
->first();
}
return $umbral;
}
private function calcularComparativos(array $periodos, array $metricasActuales): array
{
$comparativos = [];
if (count($periodos) < 2) {
return $comparativos;
}
$periodoActual = end($periodos);
$periodoAnterior = $periodos[count($periodos) - 2];
// Calcular métricas del periodo anterior
$metricasAnterior = $this->calcularTodasLasMetricas($periodoAnterior);
foreach ($metricasActuales as $key => $metrica) {
if (isset($metricasAnterior[$key])) {
$valorActual = $metrica['valor'];
$valorAnterior = $metricasAnterior[$key]['valor'];
$variacion = $valorAnterior != 0 ? ($valorActual - $valorAnterior) / abs($valorAnterior) : 0;
$comparativos[$key] = [
'valor_actual' => $valorActual,
'valor_anterior' => $valorAnterior,
'variacion_absoluta' => $valorActual - $valorAnterior,
'variacion_porcentual' => round($variacion * 100, 2),
];
}
}
// Promedio de 3 periodos si hay suficientes datos
if (count($periodos) >= 3) {
$ultimos3 = array_slice($periodos, -3);
foreach ($metricasActuales as $key => $metrica) {
$suma = 0;
foreach ($ultimos3 as $p) {
$m = $this->calcularTodasLasMetricas($p);
$suma += $m[$key]['valor'] ?? 0;
}
$comparativos[$key]['promedio_3_periodos'] = round($suma / 3, 4);
}
}
return $comparativos;
}
private function calcularFlujoEfectivo(array $periodos): array
{
if (count($periodos) < 2) {
return ['metodo' => 'indirecto', 'sin_datos' => true];
}
$actual = end($periodos);
$anterior = $periodos[count($periodos) - 2];
$balanceActual = $actual['balance_general'];
$balanceAnterior = $anterior['balance_general'];
$resultados = $actual['estado_resultados'];
// Método indirecto
$utilidadNeta = $resultados['utilidad_neta'];
$depreciacion = $balanceActual['activos_no_circulantes'] * 0.05;
// Cambios en capital de trabajo
$cambioActivosCirc = $balanceActual['activos_circulantes'] - $balanceAnterior['activos_circulantes'];
$cambioPasivosCirc = $balanceActual['pasivo_circulante'] - $balanceAnterior['pasivo_circulante'];
$flujoOperacion = $utilidadNeta + $depreciacion - $cambioActivosCirc + $cambioPasivosCirc;
// Flujo de inversión (cambio en activos no circulantes)
$flujoInversion = -($balanceActual['activos_no_circulantes'] - $balanceAnterior['activos_no_circulantes']);
// Flujo de financiamiento
$cambioDeuda = $balanceActual['total_pasivos'] - $balanceAnterior['total_pasivos'];
$cambioCapital = $balanceActual['total_capital'] - $balanceAnterior['total_capital'];
$flujoFinanciamiento = $cambioDeuda + $cambioCapital - $utilidadNeta;
return [
'metodo' => 'indirecto',
'flujo_operacion' => round($flujoOperacion, 2),
'flujo_inversion' => round($flujoInversion, 2),
'flujo_financiamiento' => round($flujoFinanciamiento, 2),
'flujo_neto' => round($flujoOperacion + $flujoInversion + $flujoFinanciamiento, 2),
'detalle' => [
'utilidad_neta' => $utilidadNeta,
'depreciacion' => $depreciacion,
'cambio_capital_trabajo' => -$cambioActivosCirc + $cambioPasivosCirc,
],
];
}
}