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') -
-
-

- Configuración -

-
-
+ -
-
+
+
@@ -77,7 +92,9 @@
+
+
@@ -131,7 +148,13 @@
+
+
+ @include('settings.isr.index', ['isrTables' => \App\Models\IsrTable::with('brackets')->get()]) +
+ +
@@ -178,8 +201,10 @@
+
-
+
+
@@ -189,32 +214,32 @@
-
Nombre:
-
{{ auth()->user()->name }}
+
Nombre:
+
{{ auth()->user()->name }}
-
Email:
-
{{ auth()->user()->email }}
+
Email:
+
{{ auth()->user()->email }}
@if(auth()->user()->razon_social) -
Empresa:
-
{{ auth()->user()->razon_social }}
+
Empresa:
+
{{ auth()->user()->razon_social }}
@endif @if(auth()->user()->fecha_ingreso) -
Ingreso:
-
{{ auth()->user()->fecha_ingreso->format('d/m/Y') }}
+
Ingreso:
+
{{ auth()->user()->fecha_ingreso->format('d/m/Y') }}
@endif -
Comisión:
-
+
Comisión:
+
{{ auth()->user()->commission_percentage }}%
-
Salario:
-
${{ number_format(auth()->user()->monthly_salary, 2) }}
+
Salario:
+
${{ number_format(auth()->user()->monthly_salary, 2) }}
-
Estado:
-
+
Estado:
+
@if(auth()->user()->is_active) Activo @else diff --git a/resources/views/settings/isr/edit.blade.php b/resources/views/settings/isr/edit.blade.php new file mode 100644 index 0000000..3f539e1 --- /dev/null +++ b/resources/views/settings/isr/edit.blade.php @@ -0,0 +1,168 @@ +@extends('layouts.app') + +@section('title', 'Editar Tabla ISR ' . $isrTable->year . ' - Configuración') + +@section('content') +
+
+
+

+ Tabla ISR {{ $isrTable->year }} +

+ + Volver + +
+
+
+ + +@if($errors->any()) + +@endif + + +
+ @csrf + @method('PUT') + +
+
+
+
+ Brackets ISR - Año {{ $isrTable->year }} +
+ 0 rango(s) +
+
+
+
+ + + + + + + + + + + + + + + + + +
Límite InferiorLímite SuperiorCuota FijaTasa (%)Acciones
+ +
+
+
+ +
+
+ + +
+
+
+ Información +
+
+
+

+ Cómo funciona la tabla ISR: +

+
    +
  • Límite Inferior: Monto mínimo del rango.
  • +
  • Límite Superior: Monto máximo (dejar vacío para "En adelante").
  • +
  • Cuota Fija: Impuesto fijo.
  • +
  • Tasa (%): Porcentaje sobre el excedente.
  • +
+
+
+ +@push('scripts') + +@endpush +@endsection \ No newline at end of file diff --git a/resources/views/settings/isr/index.blade.php b/resources/views/settings/isr/index.blade.php new file mode 100644 index 0000000..fdbd7ad --- /dev/null +++ b/resources/views/settings/isr/index.blade.php @@ -0,0 +1,169 @@ +
+
+

+ Tablas ISR +

+
+
+ + +
+
+
+
+ Tablas ISR Existentes +
+ {{ $isrTables->count() }} tabla(s) +
+
+
+ @if($isrTables->isEmpty()) +
+ No hay tablas ISR configuradas. Crea una nueva tabla para comenzar. +
+ @else +
+ + + + + + + + + + + @foreach($isrTables as $table) + + + + + + + @endforeach + +
AñoBracketsÚltima ActualizaciónAcciones
+ {{ $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 + +
+ @csrf + @method('DELETE') + +
+ +
+
+
+ @endif +
+
+ + +
+
+
+ Nueva Tabla ISR +
+
+
+
+ @csrf + +
+
+
+ + + @error('year') +
{{ $message }}
+ @enderror + Año para el cual aplica la tabla de ISR +
+
+
+
+ +
+
+
+
+
+
+ + + + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 2479d9d..2c487d0 100755 --- a/routes/web.php +++ b/routes/web.php @@ -4,6 +4,7 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\CalendarController; use App\Http\Controllers\DashboardController; use App\Http\Controllers\ExpenseController; +use App\Http\Controllers\IsrController; use App\Http\Controllers\MonthController; use App\Http\Controllers\ReportController; use App\Http\Controllers\SaleController; @@ -66,6 +67,16 @@ Route::middleware(['auth'])->group(function () { // Settings Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index'); Route::put('/settings', [SettingsController::class, 'update'])->name('settings.update'); + + // ISR Tables + Route::prefix('settings/isr')->name('settings.isr.')->group(function () { + Route::get('/', [IsrController::class, 'index'])->name('index'); + Route::post('/', [IsrController::class, 'store'])->name('store'); + Route::delete('/{isrTable}', [IsrController::class, 'destroy'])->name('destroy'); + Route::get('/{isrTable}/edit', [IsrController::class, 'edit'])->name('edit'); + Route::put('/{isrTable}/brackets', [IsrController::class, 'updateBrackets'])->name('brackets.update'); + Route::post('/{isrTable}/upload', [IsrController::class, 'uploadCsv'])->name('upload'); + }); }); // Webhook de Telegram (público, sin auth)