- 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>
420 lines
15 KiB
PHP
420 lines
15 KiB
PHP
<?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,
|
|
],
|
|
];
|
|
}
|
|
}
|