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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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',
|
||||
|
||||
65
app/Services/IsrCalculator.php
Normal file
65
app/Services/IsrCalculator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('months', function (Blueprint $table) {
|
||||
$table->foreignId('isr_table_id')->nullable()->constrained('isr_tables')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('months', function (Blueprint $table) {
|
||||
$table->dropForeign(['isr_table_id']);
|
||||
$table->dropColumn('isr_table_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -41,6 +41,19 @@
|
||||
value="{{ old('year', now()->year) }}" min="2020" max="2100" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="isr_table_id" class="form-label">Tabla ISR</label>
|
||||
<select class="form-select" id="isr_table_id" name="isr_table_id">
|
||||
<option value="">-- Sin ISR --</option>
|
||||
@foreach($isrTables as $table)
|
||||
<option value="{{ $table->id }}">
|
||||
{{ $table->year }} ({{ $table->brackets->count() }} rangos)
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<small class="text-muted">Selecciona la tabla ISR a aplicar en este mes</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">Crear Mes</button>
|
||||
<a href="{{ route('months.index') }}" class="btn btn-secondary">Cancelar</a>
|
||||
|
||||
@@ -41,6 +41,19 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="isr_table_id" class="form-label">Tabla ISR</label>
|
||||
<select class="form-select" id="isr_table_id" name="isr_table_id">
|
||||
<option value="">-- Sin ISR --</option>
|
||||
@foreach($isrTables as $table)
|
||||
<option value="{{ $table->id }}" {{ $month->isr_table_id == $table->id ? 'selected' : '' }}>
|
||||
{{ $table->year }} ({{ $table->brackets->count() }} rangos)
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<small class="text-muted">Selecciona la tabla ISR a aplicar en este mes</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Actualizar</button>
|
||||
<a href="{{ route('months.show', $month->id) }}" class="btn btn-secondary">Cancelar</a>
|
||||
|
||||
@@ -36,11 +36,20 @@
|
||||
<span class="badge bg-info">Pagado</span>
|
||||
@endif
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<small class="text-muted">ISR: </small>
|
||||
@if($month->isrTable)
|
||||
<span class="badge bg-primary">{{ $month->isrTable->year }}</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Sin ISR</span>
|
||||
@endif
|
||||
</p>
|
||||
<p class="mb-1"><small class="text-muted">Ventas: ${{ number_format($month->dailySales()->sum('user_sales'), 2) }}</small></p>
|
||||
<p class="mb-0"><small class="text-muted">Gastos: ${{ number_format($month->expenses()->sum('amount'), 2) }}</small></p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent">
|
||||
<a href="{{ route('months.show', $month->id) }}" class="btn btn-sm btn-primary">Ver Detalles</a>
|
||||
<a href="{{ route('months.edit', $month->id) }}" class="btn btn-sm btn-secondary">Editar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,31 +48,53 @@
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
@if($report['type'] === 'anticipo')
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Mitad Sueldo</h6>
|
||||
<h3>${{ number_format($report['biweekly_salary'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Comisiones del Mes</h6>
|
||||
<h3 class="text-success">+${{ number_format($report['commission_amount'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@if(isset($report['isr_amount']) && $report['isr_amount'] > 0)
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">ISR Quincenal</h6>
|
||||
<h3 class="text-danger">-${{ number_format($report['isr_amount'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Total ANTICIPO</h6>
|
||||
<h2 class="text-success">${{ number_format($report['total_earning'], 2) }}</h2>
|
||||
</div>
|
||||
@else
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Total ANTICIPO</h6>
|
||||
<h2 class="text-success">${{ number_format($report['total_earning'], 2) }}</h2>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Mitad Sueldo</h6>
|
||||
<h3>${{ number_format($report['biweekly_salary'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Gastos Q{{ $biweekly }}</h6>
|
||||
<h3 class="text-danger">-${{ number_format($report['expenses_q2'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@if(isset($report['isr_amount']) && $report['isr_amount'] > 0)
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">ISR Quincenal</h6>
|
||||
<h3 class="text-danger">-${{ number_format($report['isr_amount'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Total LIQUIDACIÓN</h6>
|
||||
<h2 class="{{ $report['total_earning'] >= 0 ? 'text-success' : 'text-danger' }}">${{ number_format($report['total_earning'], 2) }}</h2>
|
||||
</div>
|
||||
@else
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Total LIQUIDACIÓN</h6>
|
||||
<h2 class="{{ $report['total_earning'] >= 0 ? 'text-success' : 'text-danger' }}">${{ number_format($report['total_earning'], 2) }}</h2>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,6 +78,12 @@
|
||||
<td>Gastos del Mes</td>
|
||||
<td class="text-end text-danger">-${{ number_format($report['total_expenses'], 2) }}</td>
|
||||
</tr>
|
||||
@if(isset($report['isr_amount']) && $report['isr_amount'] > 0)
|
||||
<tr>
|
||||
<td>ISR ({{ $report['isr_details']['effective_rate'] ?? 0 }}%)</td>
|
||||
<td class="text-end text-danger">-${{ number_format($report['isr_amount'], 2) }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
<tr class="table-light">
|
||||
<td><strong>Total a Recibir</strong></td>
|
||||
<td class="text-end"><strong>${{ number_format($report['total_earning'], 2) }}</strong></td>
|
||||
|
||||
Reference in New Issue
Block a user