Feat: Agregada gestión de tablas ISR en settings

- Nueva tabla isr_tables y isr_brackets en BD
- Controlador IsrController para CRUD de tablas ISR
- Integración con pestaña ISR en settings
- Soporte para importación via CSV
- Captura manual de brackets
This commit is contained in:
2026-04-21 13:22:01 -06:00
parent 66df616eee
commit 4abf89c57f
11 changed files with 778 additions and 24 deletions

View File

@@ -0,0 +1,228 @@
<?php
namespace App\Http\Controllers;
use App\Models\IsrBracket;
use App\Models\IsrTable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class IsrController extends Controller
{
/**
* Lista todas las tablas ISR
*/
public function index()
{
$isrTables = IsrTable::with('brackets')->orderBy('year', 'desc')->get();
return view('settings.isr.index', compact('isrTables'));
}
/**
* Crea nueva tabla por año
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'year' => 'required|integer|min:2000|max:2100|unique:isr_tables,year',
]);
if ($validator->fails()) {
return redirect()->route('settings.index', ['tab' => 'isr'])
->withErrors($validator)
->withInput();
}
$isrTable = IsrTable::create([
'year' => $request->input('year'),
]);
return redirect()->route('settings.index', ['tab' => 'isr'])
->with('success', 'Tabla ISR para ' . $isrTable->year . ' creada. Ahora agrega los brackets.');
}
/**
* Formulario edición de brackets
*/
public function edit(IsrTable $isrTable)
{
$isrTable->load('brackets');
return view('settings.isr.edit', compact('isrTable'));
}
/**
* Guarda brackets manuales
*/
public function updateBrackets(Request $request, IsrTable $isrTable)
{
$validator = Validator::make($request->all(), [
'brackets' => 'required|array|min:1',
'brackets.*.lower_limit' => 'required|numeric|min:0',
'brackets.*.upper_limit' => 'nullable|numeric|min:0',
'brackets.*.fixed_fee' => 'required|numeric|min:0',
'brackets.*.rate' => 'required|numeric|min:0|max:100',
]);
if ($validator->fails()) {
return redirect()->route('settings.isr.edit', $isrTable)
->withErrors($validator)
->withInput();
}
// Eliminar brackets existentes
$isrTable->brackets()->delete();
// Crear nuevos brackets
$bracketsData = $request->input('brackets');
$order = 0;
// Ordenar brackets por lower_limit antes de guardar
usort($bracketsData, function ($a, $b) {
return floatval($a['lower_limit']) - floatval($b['lower_limit']);
});
foreach ($bracketsData as $bracketData) {
$isrTable->brackets()->create([
'lower_limit' => floatval($bracketData['lower_limit']),
'upper_limit' => isset($bracketData['upper_limit']) && $bracketData['upper_limit'] !== ''
? floatval($bracketData['upper_limit'])
: null,
'fixed_fee' => floatval($bracketData['fixed_fee']),
'rate' => floatval($bracketData['rate']),
'order' => $order++,
]);
}
return redirect()->route('settings.index', ['tab' => 'isr'])
->with('success', 'Brackets actualizados correctamente.');
}
/**
* Importa CSV
*/
public function uploadCsv(Request $request, IsrTable $isrTable)
{
$validator = Validator::make($request->all(), [
'csv_file' => 'required|file|mimes:csv,txt',
]);
if ($validator->fails()) {
return redirect()->route('settings.isr.edit', $isrTable)
->withErrors($validator)
->withInput();
}
$file = $request->file('csv_file');
$content = file_get_contents($file->getRealPath());
$lines = explode("\n", $content);
$brackets = [];
$firstLine = true;
foreach ($lines as $line) {
$line = trim($line);
// Ignorar primera línea (encabezados vacíos)
if ($firstLine) {
$firstLine = false;
continue;
}
if (empty($line)) {
continue;
}
// Parsear línea CSV
$values = str_getcsv($line);
if (count($values) < 4) {
continue;
}
$lowerLimit = $this->parseCsvValue($values[0]);
$upperLimit = $this->parseCsvValue($values[1]);
$fixedFee = $this->parseCsvValue($values[2]);
$rate = $this->parseCsvValue($values[3]);
// Ignorar si no hay lower_limit válido
if ($lowerLimit === null || $lowerLimit < 0) {
continue;
}
// Default null values to 0
$fixedFee = $fixedFee ?? 0;
$rate = $rate ?? 0;
$brackets[] = [
'lower_limit' => $lowerLimit,
'upper_limit' => $upperLimit,
'fixed_fee' => $fixedFee,
'rate' => $rate,
];
}
// Ordenar brackets por lower_limit
usort($brackets, function ($a, $b) {
return $a['lower_limit'] - $b['lower_limit'];
});
if (empty($brackets)) {
return redirect()->route('settings.index', ['tab' => 'isr'])
->with('error', 'No se encontraron brackets válidos en el archivo CSV.');
}
// Eliminar brackets existentes
$isrTable->brackets()->delete();
// Crear nuevos brackets
$order = 0;
foreach ($brackets as $bracket) {
$isrTable->brackets()->create([
'lower_limit' => $bracket['lower_limit'],
'upper_limit' => $bracket['upper_limit'],
'fixed_fee' => $bracket['fixed_fee'],
'rate' => $bracket['rate'],
'order' => $order++,
]);
}
return redirect()->route('settings.index', ['tab' => 'isr'])
->with('success', 'Brackets importados correctamente desde el CSV.');
}
/**
* Elimina tabla ISR
*/
public function destroy(IsrTable $isrTable)
{
$year = $isrTable->year;
$isrTable->delete();
return redirect()->route('settings.index', ['tab' => 'isr'])
->with('success', 'Tabla ISR para ' . $year . ' eliminada.');
}
/**
* Convierte valor CSV como "3,537.15" a 3537.15
*/
private function parseCsvValue(string $val): ?float
{
// Limpiar comillas y espacios
$val = trim($val, '"\' ');
// Manejar "En adelante" como null
if (in_array(strtolower($val), ['en adelante', 'en adelante ', 'null', ''])) {
return null;
}
// Eliminar comas de miles
$val = str_replace(',', '', $val);
// Convertir a número
$num = floatval($val);
// Permitir 0 como valor válido, pero null para valores negativos o no numéricos
return is_numeric($val) ? $num : null;
}
}