diff --git a/app/Http/Controllers/IsrController.php b/app/Http/Controllers/IsrController.php new file mode 100644 index 0000000..0274f9f --- /dev/null +++ b/app/Http/Controllers/IsrController.php @@ -0,0 +1,228 @@ +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; + } +} \ No newline at end of file diff --git a/app/Models/IsrBracket.php b/app/Models/IsrBracket.php new file mode 100644 index 0000000..1878f59 --- /dev/null +++ b/app/Models/IsrBracket.php @@ -0,0 +1,36 @@ + + */ + protected function casts(): array + { + return [ + 'lower_limit' => 'decimal:2', + 'upper_limit' => 'decimal:2', + 'fixed_fee' => 'decimal:2', + 'rate' => 'decimal:2', + ]; + } + + /** + * Relación con la tabla ISR + */ + public function isrTable(): BelongsTo + { + return $this->belongsTo(IsrTable::class); + } +} \ No newline at end of file diff --git a/app/Models/IsrTable.php b/app/Models/IsrTable.php new file mode 100644 index 0000000..c0d0d5f --- /dev/null +++ b/app/Models/IsrTable.php @@ -0,0 +1,21 @@ +hasMany(IsrBracket::class)->orderBy('order'); + } +} \ No newline at end of file diff --git a/csv/ISR2026.csv b/csv/ISR2026.csv new file mode 100644 index 0000000..3dbc20e --- /dev/null +++ b/csv/ISR2026.csv @@ -0,0 +1,12 @@ +,,, +0.01,416.7,0,1.92 +416.71,"3,537.15",7.95,6.4 +"3,537.16","6,216.15",207.75,10.88 +"6,216.16","7,225.95",499.2,16 +"7,225.96","8,651.40",660.75,17.92 +"8,651.41","17,448.75",916.2,21.36 +"17,448.76","27,501.60","2,795.25",23.52 +"27,501.61","52,505.25","5,159.70",30 +"52,505.26","70,006.95","12,660.75",32 +"70,006.96","210,020.70","18,261.30",34 +"210,020.71",En adelante,"65,866.05",35 \ No newline at end of file diff --git a/database/migrations/2024_01_01_000010_create_isr_tables_table.php b/database/migrations/2024_01_01_000010_create_isr_tables_table.php new file mode 100644 index 0000000..ad12e95 --- /dev/null +++ b/database/migrations/2024_01_01_000010_create_isr_tables_table.php @@ -0,0 +1,28 @@ +id(); + $table->year('year')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('isr_tables'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_01_000011_create_isr_brackets_table.php b/database/migrations/2024_01_01_000011_create_isr_brackets_table.php new file mode 100644 index 0000000..98daee9 --- /dev/null +++ b/database/migrations/2024_01_01_000011_create_isr_brackets_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('isr_table_id')->constrained()->onDelete('cascade'); + $table->decimal('lower_limit', 15, 2); + $table->decimal('upper_limit', 15, 2)->nullable()->comment('null significa "En adelante"'); + $table->decimal('fixed_fee', 12, 2)->default(0); + $table->decimal('rate', 5, 2)->comment('porcentaje'); + $table->unsignedInteger('order')->default(0); + $table->index('isr_table_id'); + $table->index('order'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('isr_brackets'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_04_21_000001_add_timestamps_to_isr_brackets_table.php b/database/migrations/2026_04_21_000001_add_timestamps_to_isr_brackets_table.php new file mode 100644 index 0000000..031a4a1 --- /dev/null +++ b/database/migrations/2026_04_21_000001_add_timestamps_to_isr_brackets_table.php @@ -0,0 +1,22 @@ +timestamps(); + }); + } + + public function down(): void + { + Schema::table('isr_brackets', function (Blueprint $table) { + $table->dropTimestamps(); + }); + } +}; \ No newline at end of file diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php index f6eb02a..ecad5a4 100755 --- a/resources/views/settings/index.blade.php +++ b/resources/views/settings/index.blade.php @@ -3,16 +3,31 @@ @section('title', 'Configuración') @section('content') -
+ Cómo funciona la tabla ISR: +
+| Año | +Brackets | +Última Actualización | +Acciones | +
|---|---|---|---|
| + {{ $table->year }} + | ++ {{ $table->brackets->count() }} rango(s) + | ++ + @if($table->brackets->isNotEmpty()) + {{ $table->brackets->max('updated_at')->format('d/m/Y H:i') }} + @else + Sin datos + @endif + + | +
+
+
+ Editar
+
+
+
+
+ |
+