214 lines
6.7 KiB
PHP
214 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
|
|
class PayrollRun extends BaseModel
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $fillable = [
|
|
'title',
|
|
'payroll_frequency',
|
|
'pay_period_start',
|
|
'pay_period_end',
|
|
'pay_date',
|
|
'total_gross_pay',
|
|
'total_deductions',
|
|
'total_net_pay',
|
|
'employee_count',
|
|
'status',
|
|
'notes',
|
|
'created_by',
|
|
];
|
|
|
|
protected $casts = [
|
|
'pay_period_start' => 'date:Y-m-d',
|
|
'pay_period_end' => 'date:Y-m-d',
|
|
'pay_date' => 'date:Y-m-d',
|
|
'total_gross_pay' => 'decimal:2',
|
|
'total_deductions' => 'decimal:2',
|
|
'total_net_pay' => 'decimal:2',
|
|
];
|
|
|
|
/**
|
|
* Get the payroll entries.
|
|
*/
|
|
public function payrollEntries()
|
|
{
|
|
return $this->hasMany(PayrollEntry::class);
|
|
}
|
|
|
|
/**
|
|
* Get the payslips through payroll entries.
|
|
*/
|
|
public function payslips()
|
|
{
|
|
return $this->hasManyThrough(Payslip::class, PayrollEntry::class);
|
|
}
|
|
|
|
/**
|
|
* Get the user who created the payroll run.
|
|
*/
|
|
public function creator()
|
|
{
|
|
return $this->belongsTo(User::class, 'created_by');
|
|
}
|
|
|
|
/**
|
|
* Calculate and update totals.
|
|
*/
|
|
public function calculateTotals()
|
|
{
|
|
$entries = $this->payrollEntries;
|
|
|
|
$this->total_gross_pay = $entries->sum('gross_pay');
|
|
$this->total_deductions = $entries->sum('total_deductions');
|
|
$this->total_net_pay = $entries->sum('net_pay');
|
|
$this->employee_count = $entries->count();
|
|
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Process payroll for all employees.
|
|
*/
|
|
public function processPayroll()
|
|
{
|
|
if ($this->status !== 'draft') {
|
|
return false;
|
|
}
|
|
|
|
$this->status = 'draft';
|
|
$this->save();
|
|
|
|
try {
|
|
// Get all active employees
|
|
$employees = User::with('employee')->where('type', 'employee')
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->whereHas('employee', function ($q) {
|
|
$q->whereIn('employee_status', ['active', 'probation']);
|
|
})
|
|
->orderby('id', 'desc')
|
|
->get();
|
|
|
|
foreach ($employees as $employee) {
|
|
$this->processEmployeePayroll($employee);
|
|
}
|
|
|
|
$this->calculateTotals();
|
|
$this->status = 'completed';
|
|
$this->save();
|
|
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
$this->status = 'draft';
|
|
$this->save();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process payroll for individual employee.
|
|
*/
|
|
private function processEmployeePayroll($employee)
|
|
{
|
|
// Skip if entry already exists for this employee in this run
|
|
$existingEntry = PayrollEntry::where('payroll_run_id', $this->id)
|
|
->where('employee_id', $employee->id)
|
|
->exists();
|
|
|
|
if ($existingEntry) {
|
|
return;
|
|
}
|
|
|
|
// Call the unified DOLE compliant calculation engine
|
|
$service = new \App\Services\PayrollService();
|
|
$salaryBreakdown = $service->calculateForPeriod(
|
|
$employee->employee,
|
|
$this->pay_period_start->format('Y-m-d'),
|
|
$this->pay_period_end->format('Y-m-d')
|
|
);
|
|
|
|
if (!$salaryBreakdown || !isset($salaryBreakdown['basic_salary'])) {
|
|
return;
|
|
}
|
|
|
|
$summary = $salaryBreakdown['summary'] ?? [];
|
|
|
|
// Determine pure dynamically added component earnings (anything above basic)
|
|
$componentEarnings = $salaryBreakdown['total_earnings'] - $salaryBreakdown['basic_salary'];
|
|
|
|
// Persist payroll entry mapped to PayrollService output
|
|
PayrollEntry::create([
|
|
'payroll_run_id' => $this->id,
|
|
'employee_id' => $employee->id,
|
|
'basic_salary' => $salaryBreakdown['basic_salary'],
|
|
'component_earnings' => $componentEarnings,
|
|
'total_earnings' => $salaryBreakdown['total_earnings'],
|
|
'total_deductions' => $salaryBreakdown['total_deductions'],
|
|
'gross_pay' => $salaryBreakdown['total_earnings'],
|
|
'net_pay' => $salaryBreakdown['net_pay'],
|
|
|
|
'working_days' => ($summary['days_worked'] ?? 0) + ($summary['absences'] ?? 0),
|
|
'present_days' => $summary['days_worked'] ?? 0,
|
|
'full_present_days' => $summary['days_worked'] ?? 0,
|
|
'half_days' => 0,
|
|
'holiday_days' => $summary['holiday'] ?? 0,
|
|
'paid_leave_days' => $summary['leaves'] ?? 0,
|
|
'unpaid_leave_days' => 0,
|
|
'absent_days' => $summary['absences'] ?? 0,
|
|
|
|
'overtime_hours' => 0,
|
|
'overtime_amount' => $summary['reg_ot'] ?? 0,
|
|
'per_day_salary' => $salaryBreakdown['daily_rate'],
|
|
'unpaid_leave_deduction' => $summary['absence_amount'] ?? 0,
|
|
|
|
'earnings_breakdown' => $salaryBreakdown['earnings'],
|
|
'deductions_breakdown' => $salaryBreakdown['deductions'],
|
|
'created_by' => $this->created_by,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get employee leave data for pay period.
|
|
*/
|
|
private function getEmployeeLeaveData($employeeId)
|
|
{
|
|
$leaveApplications = \App\Models\LeaveApplication::where('employee_id', $employeeId)
|
|
->where('status', 'approved')
|
|
->where(function ($query) {
|
|
$query->whereBetween('start_date', [$this->pay_period_start, $this->pay_period_end])
|
|
->orWhereBetween('end_date', [$this->pay_period_start, $this->pay_period_end])
|
|
->orWhere(function ($q) {
|
|
$q->where('start_date', '<=', $this->pay_period_start)
|
|
->where('end_date', '>=', $this->pay_period_end);
|
|
});
|
|
})
|
|
->with('leaveType')
|
|
->get();
|
|
|
|
$paidLeaveDays = 0;
|
|
$unpaidLeaveDays = 0;
|
|
|
|
foreach ($leaveApplications as $leave) {
|
|
// Calculate days within pay period
|
|
$leaveStart = max($leave->start_date, $this->pay_period_start);
|
|
$leaveEnd = min($leave->end_date, $this->pay_period_end);
|
|
$leaveDays = $leaveStart->diffInDays($leaveEnd) + 1;
|
|
|
|
if ($leave->leaveType->is_paid) {
|
|
$paidLeaveDays += $leaveDays;
|
|
} else {
|
|
$unpaidLeaveDays += $leaveDays;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'paid_leave_days' => $paidLeaveDays,
|
|
'unpaid_leave_days' => $unpaidLeaveDays,
|
|
];
|
|
}
|
|
}
|