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,180 @@
<?php
namespace App\Services\Parsers;
use PhpOffice\PhpSpreadsheet\IOFactory;
class GenericoParser implements ParserInterface
{
private array $mapeoColumnas = [
'codigo' => ['codigo', 'cuenta', 'code', 'account', 'cta', 'numero'],
'nombre' => ['nombre', 'descripcion', 'name', 'description', 'concepto'],
'saldo_inicial_deudor' => ['saldo_inicial_deudor', 'inicial_debe', 'opening_debit', 'si_deudor'],
'saldo_inicial_acreedor' => ['saldo_inicial_acreedor', 'inicial_haber', 'opening_credit', 'si_acreedor'],
'cargos' => ['cargos', 'debe', 'debit', 'debits', 'movs_deudor'],
'abonos' => ['abonos', 'haber', 'credit', 'credits', 'movs_acreedor'],
'saldo_final_deudor' => ['saldo_final_deudor', 'final_debe', 'closing_debit', 'sf_deudor'],
'saldo_final_acreedor' => ['saldo_final_acreedor', 'final_haber', 'closing_credit', 'sf_acreedor'],
];
public function getSistema(): string
{
return 'generico';
}
public function puedeManej(string $filePath): bool
{
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
return in_array($extension, ['xlsx', 'xls', 'csv']);
}
public function parsear(string $filePath): array
{
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if ($extension === 'csv') {
return $this->parsearCsv($filePath);
}
return $this->parsearExcel($filePath);
}
private function parsearExcel(string $filePath): array
{
$spreadsheet = IOFactory::load($filePath);
$worksheet = $spreadsheet->getActiveSheet();
$rows = $worksheet->toArray();
if (empty($rows)) {
return [];
}
// Primera fila son headers
$headers = array_map(fn($h) => $this->normalizarHeader($h), $rows[0]);
$mapeo = $this->mapearColumnas($headers);
$cuentas = [];
for ($i = 1; $i < count($rows); $i++) {
$cuenta = $this->parsearFila($rows[$i], $mapeo);
if ($cuenta) {
$cuentas[] = $cuenta;
}
}
return $cuentas;
}
private function parsearCsv(string $filePath): array
{
$handle = fopen($filePath, 'r');
if (!$handle) {
throw new \Exception('No se pudo abrir el archivo CSV');
}
// Detectar delimitador
$primeraLinea = fgets($handle);
rewind($handle);
$delimitador = $this->detectarDelimitador($primeraLinea);
// Primera fila son headers
$headers = fgetcsv($handle, 0, $delimitador);
$headers = array_map(fn($h) => $this->normalizarHeader($h), $headers);
$mapeo = $this->mapearColumnas($headers);
$cuentas = [];
while (($row = fgetcsv($handle, 0, $delimitador)) !== false) {
$cuenta = $this->parsearFila($row, $mapeo);
if ($cuenta) {
$cuentas[] = $cuenta;
}
}
fclose($handle);
return $cuentas;
}
private function normalizarHeader(?string $header): string
{
if ($header === null) {
return '';
}
return strtolower(trim(preg_replace('/[^a-zA-Z0-9]/', '_', $header)));
}
private function detectarDelimitador(string $linea): string
{
$delimitadores = [',', ';', "\t", '|'];
$conteos = [];
foreach ($delimitadores as $d) {
$conteos[$d] = substr_count($linea, $d);
}
return array_keys($conteos, max($conteos))[0];
}
private function mapearColumnas(array $headers): array
{
$mapeo = [];
foreach ($this->mapeoColumnas as $campo => $aliases) {
foreach ($headers as $index => $header) {
if (in_array($header, $aliases)) {
$mapeo[$campo] = $index;
break;
}
}
}
return $mapeo;
}
private function parsearFila(array $row, array $mapeo): ?array
{
// Verificar que tenemos código y nombre
if (!isset($mapeo['codigo']) || !isset($mapeo['nombre'])) {
return null;
}
$codigo = trim($row[$mapeo['codigo']] ?? '');
$nombre = trim($row[$mapeo['nombre']] ?? '');
if (empty($codigo) || empty($nombre)) {
return null;
}
// Determinar nivel basado en la estructura del código
$nivel = $this->determinarNivel($codigo);
$esCuentaPadre = $nivel <= 2;
return [
'codigo' => $codigo,
'nombre' => $nombre,
'nivel' => $nivel,
'saldo_inicial_deudor' => $this->obtenerNumero($row, $mapeo, 'saldo_inicial_deudor'),
'saldo_inicial_acreedor' => $this->obtenerNumero($row, $mapeo, 'saldo_inicial_acreedor'),
'cargos' => $this->obtenerNumero($row, $mapeo, 'cargos'),
'abonos' => $this->obtenerNumero($row, $mapeo, 'abonos'),
'saldo_final_deudor' => $this->obtenerNumero($row, $mapeo, 'saldo_final_deudor'),
'saldo_final_acreedor' => $this->obtenerNumero($row, $mapeo, 'saldo_final_acreedor'),
'es_cuenta_padre' => $esCuentaPadre,
];
}
private function determinarNivel(string $codigo): int
{
// Contar separadores para determinar nivel
$separadores = preg_match_all('/[-.\s]/', $codigo);
return min(3, $separadores + 1);
}
private function obtenerNumero(array $row, array $mapeo, string $campo): float
{
if (!isset($mapeo[$campo])) {
return 0.0;
}
$valor = $row[$mapeo[$campo]] ?? 0;
return (float) str_replace([',', '$', ' '], '', $valor);
}
}