Feat: Implementado cálculo de ISR en nóminas

- Agregado campo isr_table_id en tabla months para seleccionar tabla ISR por mes
- Creado servicio IsrCalculator para calcular ISR mensual y quincenal
- Modificado CommissionCalculator para descontar ISR del total a pagar
- Agregado selector de tabla ISR en formulario de crear/editar mes
- Actualizada vista de meses para mostrar tabla ISR asignada
- Actualizados reportes mensual y quincenal para mostrar ISR descontado
This commit is contained in:
2026-04-21 13:45:39 -06:00
parent 4abf89c57f
commit e8aac6eaa5
10 changed files with 213 additions and 21 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Models\IsrTable;
use App\Models\Month;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -16,6 +17,7 @@ class MonthController extends Controller
$user = Auth::user();
$months = $user->months()
->with('isrTable')
->orderBy('year', 'desc')
->orderByRaw("FIELD(name, 'Diciembre', 'Noviembre', 'Octubre', 'Septiembre', 'Agosto', 'Julio', 'Junio', 'Mayo', 'Abril', 'Marzo', 'Febrero', 'Enero')")
->paginate(12);
@@ -28,7 +30,8 @@ class MonthController extends Controller
*/
public function create()
{
return view('months.create');
$isrTables = IsrTable::with('brackets')->orderBy('year', 'desc')->get();
return view('months.create', compact('isrTables'));
}
/**
@@ -41,6 +44,7 @@ class MonthController extends Controller
$validated = $request->validate([
'name' => ['required', 'string', 'max:50'],
'year' => ['required', 'integer', 'min:2020', 'max:2100'],
'isr_table_id' => ['nullable', 'exists:isr_tables,id'],
]);
// Verificar que no exista ya el mes para el usuario
@@ -59,6 +63,7 @@ class MonthController extends Controller
'name' => $validated['name'],
'year' => $validated['year'],
'status' => 'open',
'isr_table_id' => $validated['isr_table_id'] ?? null,
]);
return redirect()->route('months.index')
@@ -92,7 +97,8 @@ class MonthController extends Controller
abort(403);
}
return view('months.edit', compact('month'));
$isrTables = IsrTable::with('brackets')->orderBy('year', 'desc')->get();
return view('months.edit', compact('month', 'isrTables'));
}
/**
@@ -110,6 +116,7 @@ class MonthController extends Controller
'name' => ['required', 'string', 'max:50'],
'year' => ['required', 'integer', 'min:2020', 'max:2100'],
'status' => ['required', 'in:open,closed,paid'],
'isr_table_id' => ['nullable', 'exists:isr_tables,id'],
]);
$month->update($validated);

View File

@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['user_id', 'name', 'year', 'status'])]
#[Fillable(['user_id', 'name', 'year', 'status', 'isr_table_id'])]
class Month extends Model
{
use HasFactory;
@@ -25,6 +25,7 @@ class Month extends Model
'name',
'year',
'status',
'isr_table_id',
];
/**
@@ -63,6 +64,14 @@ class Month extends Model
return $this->hasMany(Expense::class);
}
/**
* Relación con tabla ISR
*/
public function isrTable(): BelongsTo
{
return $this->belongsTo(IsrTable::class);
}
/**
* Obtener el nombre del mes con formato
*/

View File

@@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\DailySale;
use App\Models\Expense;
use App\Models\IsrTable;
use App\Models\Month;
use App\Models\User;
use Illuminate\Support\Facades\DB;
@@ -38,8 +39,15 @@ class CommissionCalculator
// Calcular comisión basada en ventas del sistema (ventas consolidadas)
$commission = ($totalSystemSales * $commissionPercentage) / 100;
// Calcular percepción total (salario + comisión - gastos)
$totalEarning = $monthlySalary + $commission - $totalExpenses;
// Calcular ISR sobre (salario + comisión)
// Los gastos personales NO son deducibles del ISR
$incomeForIsr = $monthlySalary + $commission;
$isrTable = $month->isrTable;
$isrResult = IsrCalculator::calculateMonthly($incomeForIsr, $isrTable);
$isrAmount = $isrResult['isr'];
// Calcular percepción total con ISR (salario + comisión - gastos - ISR)
$totalEarning = $monthlySalary + $commission - $totalExpenses - $isrAmount;
return [
'user_id' => $user->id,
@@ -51,6 +59,8 @@ class CommissionCalculator
'monthly_salary' => round($monthlySalary, 2),
'commission_percentage' => round($commissionPercentage, 2),
'commission_amount' => round($commission, 2),
'isr_amount' => round($isrAmount, 2),
'isr_details' => $isrResult,
'total_earning' => round($totalEarning, 2),
'has_difference' => ($totalUserSales !== $totalSystemSales),
'sales_difference' => round($totalUserSales - $totalSystemSales, 2),
@@ -60,8 +70,8 @@ class CommissionCalculator
/**
* Calcular quincena (primera o segunda)
*
* QUINCENA 1 (ANTICIPO): mitad salary + comisiones del MES completo
* QUINCENA 2 (LIQUIDACIÓN): mitad salary - gastos de la segunda quincena
* QUINCENA 1 (ANTICIPO): mitad salary + comisiones del MES completo - ISR quincenal
* QUINCENA 2 (LIQUIDACIÓN): mitad salary - gastos de la segunda quincena - ISR quincenal
*/
public static function calculateBiweekly(User $user, Month $month, int $biweekly): array
{
@@ -78,13 +88,20 @@ class CommissionCalculator
$year = $month->year;
$lastDay = self::getLastDayOfMonth($month->name, $year);
// Calcular ISR mensual completo (salario + comisión)
$totalSystemSales = $month->dailySales()->sum('system_sales');
$commission = ($totalSystemSales * $commissionPercentage) / 100;
$incomeForIsr = $monthlySalary + $commission;
$isrTable = $month->isrTable;
$isrResult = IsrCalculator::calculateMonthly($incomeForIsr, $isrTable);
$isrMonthlyAmount = $isrResult['isr'];
$isrBiweekly = IsrCalculator::calculateBiweekly($isrMonthlyAmount);
if ($biweekly === 1) {
// =====================
// QUINCENA 1 - ANTICIPO
// =====================
// Anticipo = mitad del sueldo + comisiones del MES completo
$totalSystemSales = $month->dailySales()->sum('system_sales');
$commission = ($totalSystemSales * $commissionPercentage) / 100;
// Anticipo = mitad del sueldo + comisiones del MES completo - ISR quincenal
// Gastos: q1 completo + mensual/2
$expensesQ1Amount = $month->expenses()
@@ -97,7 +114,7 @@ class CommissionCalculator
return $e->expense_type === 'mensual' ? $e->amount / 2 : $e->amount;
});
$totalEarning = $biweeklySalary + $commission - $expensesQ1Amount;
$totalEarning = $biweeklySalary + $commission - $expensesQ1Amount - $isrBiweekly;
return [
'user_id' => $user->id,
@@ -105,11 +122,12 @@ class CommissionCalculator
'month_name' => $month->name . ' ' . $year,
'biweekly' => $biweekly,
'period' => '1ra Quincena (1-15) - ANTICIPO',
'description' => 'Mitad del sueldo + comisiones del mes completo',
'description' => 'Mitad del sueldo + comisiones del mes completo - ISR',
'biweekly_salary' => round($biweeklySalary, 2),
'total_system_sales' => round($totalSystemSales, 2),
'commission_percentage' => round($commissionPercentage, 2),
'commission_amount' => round($commission, 2),
'isr_amount' => round($isrBiweekly, 2),
'total_expenses_month' => 0,
'expenses_q1' => round($expensesQ1Amount, 2),
'expenses_q2' => 0,
@@ -120,7 +138,7 @@ class CommissionCalculator
// =====================
// QUINCENA 2 - LIQUIDACIÓN
// =====================
// Liquidación = mitad del sueldo - gastos de Q2
// Liquidación = mitad del sueldo - gastos de Q2 - ISR quincenal
// Gastos: q2 completo + mensual/2
$expensesQ2Amount = $month->expenses()
@@ -134,7 +152,7 @@ class CommissionCalculator
});
// Total a pagar en liquidacion
$totalEarning = $biweeklySalary - $expensesQ2Amount;
$totalEarning = $biweeklySalary - $expensesQ2Amount - $isrBiweekly;
return [
'user_id' => $user->id,
@@ -142,9 +160,10 @@ class CommissionCalculator
'month_name' => $month->name . ' ' . $year,
'biweekly' => $biweekly,
'period' => "2da Quincena (16-$lastDay) - LIQUIDACIÓN",
'description' => 'Mitad del sueldo - mitad de gastos del mes',
'description' => 'Mitad del sueldo - mitad de gastos del mes - ISR',
'biweekly_salary' => round($biweeklySalary, 2),
'total_system_sales' => 0,
'isr_amount' => round($isrBiweekly, 2),
'expenses_q2' => round($expensesQ2Amount, 2),
'total_earning' => round($totalEarning, 2),
'type' => 'liquidacion',

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Services;
use App\Models\IsrBracket;
use App\Models\IsrTable;
class IsrCalculator
{
/**
* Calcular ISR mensual dado un ingreso base
* Retorna: ['isr' => float, 'bracket' => array|null, 'effective_rate' => float]
*/
public static function calculateMonthly(float $baseIncome, ?IsrTable $isrTable): array
{
if (!$isrTable || $baseIncome <= 0) {
return [
'isr' => 0,
'bracket' => null,
'effective_rate' => 0,
'taxable_income' => 0,
];
}
$brackets = $isrTable->brackets()->orderBy('lower_limit')->get();
foreach ($brackets as $bracket) {
$upperLimit = $bracket->upper_limit;
// Si es el último bracket (upper_limit es null = "En adelante")
if ($upperLimit === null || $baseIncome <= $upperLimit) {
// Calcular ISR: Cuota Fija + (Excedente × Tasa)
$excedent = $baseIncome - $bracket->lower_limit;
$isr = $bracket->fixed_fee + ($excedent * $bracket->rate / 100);
return [
'isr' => round($isr, 2),
'bracket' => [
'lower_limit' => $bracket->lower_limit,
'upper_limit' => $bracket->upper_limit,
'fixed_fee' => $bracket->fixed_fee,
'rate' => $bracket->rate,
],
'effective_rate' => round(($isr / $baseIncome) * 100, 2),
'taxable_income' => round($baseIncome, 2),
];
}
}
return [
'isr' => 0,
'bracket' => null,
'effective_rate' => 0,
'taxable_income' => 0,
];
}
/**
* Calcular ISR quincenal = ISR mensual / 2
*/
public static function calculateBiweekly(float $monthlyIsr): float
{
return round($monthlyIsr / 2, 2);
}
}