468 lines
18 KiB
PHP
468 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Employee;
|
|
use App\Models\EmployeeFinancial;
|
|
use App\Models\EmployeeWorkLog;
|
|
use App\Models\Currency;
|
|
use App\Models\PayrollPeriod;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class PayrollController extends Controller
|
|
{
|
|
/**
|
|
* Display payroll dashboard with stats.
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
$currentYear = now()->year;
|
|
$currentMonth = now()->month;
|
|
|
|
$year = $request->get('year', $currentYear);
|
|
$month = $request->get('month', $currentMonth);
|
|
|
|
// Statistics for the selected month/year
|
|
$financials = EmployeeFinancial::with(['employee', 'currency'])
|
|
->where('tenant_id', $this->getTenantId())
|
|
->where('month', $month)
|
|
->where('year', $year)
|
|
->where('is_settlement', false)
|
|
->orderBy('employee_id')
|
|
->orderBy('type')
|
|
->paginate(30)
|
|
->appends($request->query());
|
|
|
|
// Calculate totals for the month
|
|
$allMonthFinancials = EmployeeFinancial::where('tenant_id', $this->getTenantId())
|
|
->where('month', $month)
|
|
->where('year', $year)
|
|
->where('is_settlement', false)
|
|
->get();
|
|
|
|
$totalGross = $allMonthFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount');
|
|
$totalDeductions = $allMonthFinancials->where('type', 'deduction')->sum('amount');
|
|
$totalAdvances = $allMonthFinancials->where('type', 'advance')->sum('amount');
|
|
$totalNet = $totalGross - $totalDeductions - $totalAdvances;
|
|
$totalPaid = $allMonthFinancials->where('is_paid', true)->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount');
|
|
$totalUnpaid = $totalGross - $totalPaid;
|
|
|
|
// Employee count with financials this month
|
|
$activeEmployeeCount = $allMonthFinancials->pluck('employee_id')->unique()->count();
|
|
$unpaidEmployeeCount = $allMonthFinancials->where('is_paid', false)->pluck('employee_id')->unique()->count();
|
|
|
|
$employees = Employee::where('tenant_id', $this->getTenantId())
|
|
->where('status', 'active')
|
|
->get();
|
|
$months = range(1, 12);
|
|
$years = range($currentYear - 2, $currentYear + 1);
|
|
|
|
// Persian month names
|
|
$persianMonths = [
|
|
1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد',
|
|
4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور',
|
|
7 => 'مهر', 8 => 'آبان', 9 => 'آذر',
|
|
10 => 'دی', 11 => 'بهمن', 12 => 'اسفند'
|
|
];
|
|
|
|
return view('payroll.index', compact(
|
|
'financials', 'employees', 'months', 'years', 'year', 'month',
|
|
'totalGross', 'totalDeductions', 'totalAdvances', 'totalNet',
|
|
'totalPaid', 'totalUnpaid', 'activeEmployeeCount', 'unpaidEmployeeCount',
|
|
'persianMonths'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Show payroll calculation form.
|
|
*/
|
|
public function calculate(Request $request)
|
|
{
|
|
$employees = Employee::where('tenant_id', $this->getTenantId())
|
|
->where('status', 'active')
|
|
->with(['financials' => function ($q) {
|
|
$q->where('month', now()->month)->where('year', now()->year);
|
|
}])
|
|
->get();
|
|
$currencies = Currency::where('is_active', true)->get();
|
|
|
|
$currentMonth = now()->month;
|
|
$currentYear = now()->year;
|
|
|
|
return view('payroll.calculate', compact('employees', 'currencies', 'currentMonth', 'currentYear'));
|
|
}
|
|
|
|
/**
|
|
* Auto-calculate payroll for a month.
|
|
* Generates salary entries for all active employees based on base_salary + work logs.
|
|
*/
|
|
public function autoCalculate(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'month' => 'required|integer|min:1|max:12',
|
|
'year' => 'required|integer|min:1400|max:1500',
|
|
'employee_ids' => 'nullable|array',
|
|
'employee_ids.*' => 'exists:employees,id',
|
|
'include_worklogs' => 'nullable|boolean',
|
|
]);
|
|
|
|
$tenantId = $this->getTenantId();
|
|
$month = $validated['month'];
|
|
$year = $validated['year'];
|
|
|
|
// Get employees to process
|
|
$query = Employee::where('tenant_id', $tenantId)->where('status', 'active');
|
|
if (!empty($validated['employee_ids'])) {
|
|
$query->whereIn('id', $validated['employee_ids']);
|
|
}
|
|
$employees = $query->get();
|
|
|
|
$created = 0;
|
|
$skipped = 0;
|
|
|
|
foreach ($employees as $employee) {
|
|
// Check if salary already exists for this month
|
|
$existingSalary = EmployeeFinancial::where('tenant_id', $tenantId)
|
|
->where('employee_id', $employee->id)
|
|
->where('type', 'salary')
|
|
->where('month', $month)
|
|
->where('year', $year)
|
|
->where('is_settlement', false)
|
|
->first();
|
|
|
|
if ($existingSalary) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
// Calculate overtime from work logs
|
|
$overtimeHours = 0;
|
|
$regularHours = 0;
|
|
if (!empty($validated['include_worklogs'])) {
|
|
$workLogs = EmployeeWorkLog::where('employee_id', $employee->id)
|
|
->whereMonth('date', $month)
|
|
->whereYear('date', $year)
|
|
->get();
|
|
$regularHours = $workLogs->sum('hours');
|
|
$overtimeHours = $workLogs->sum('overtime_hours');
|
|
}
|
|
|
|
// Get currency
|
|
$currencyId = $employee->currency_id ?? Currency::where('is_default', true)->value('id');
|
|
|
|
// Create salary entry
|
|
if ($employee->base_salary > 0) {
|
|
EmployeeFinancial::create([
|
|
'tenant_id' => $tenantId,
|
|
'employee_id' => $employee->id,
|
|
'type' => 'salary',
|
|
'amount' => $employee->base_salary,
|
|
'currency_id' => $currencyId,
|
|
'effective_date' => now(),
|
|
'month' => $month,
|
|
'year' => $year,
|
|
'is_paid' => false,
|
|
'created_by' => Auth::id(),
|
|
'description' => __('payroll.auto_salary_description', ['month' => $month, 'year' => $year]),
|
|
]);
|
|
$created++;
|
|
}
|
|
|
|
// Create overtime entry if there are overtime hours
|
|
if ($overtimeHours > 0 && $employee->base_salary > 0) {
|
|
$hourlyRate = $employee->base_salary / 200; // rough estimate: 200 working hours/month
|
|
$overtimeAmount = round($overtimeHours * $hourlyRate * 1.4); // 1.4x for overtime
|
|
|
|
EmployeeFinancial::create([
|
|
'tenant_id' => $tenantId,
|
|
'employee_id' => $employee->id,
|
|
'type' => 'overtime',
|
|
'amount' => $overtimeAmount,
|
|
'currency_id' => $currencyId,
|
|
'effective_date' => now(),
|
|
'month' => $month,
|
|
'year' => $year,
|
|
'is_paid' => false,
|
|
'created_by' => Auth::id(),
|
|
'description' => __('payroll.auto_overtime_description', ['hours' => $overtimeHours]),
|
|
]);
|
|
}
|
|
}
|
|
|
|
$message = __('payroll.auto_calculated', ['created' => $created, 'skipped' => $skipped]);
|
|
|
|
return redirect()->route('payroll.index', ['month' => $month, 'year' => $year])
|
|
->with('success', $message);
|
|
}
|
|
|
|
/**
|
|
* Store a new payroll entry.
|
|
*/
|
|
public function store(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'employee_id' => 'required|exists:employees,id',
|
|
'type' => 'required|in:salary,overtime,bonus,deduction,advance,allowance',
|
|
'amount' => 'required|numeric|min:0',
|
|
'currency_id' => 'nullable|exists:currencies,id',
|
|
'description' => 'nullable|string',
|
|
'effective_date' => 'required|date',
|
|
'month' => 'required|integer|min:1|max:12',
|
|
'year' => 'required|integer|min:1400|max:1500',
|
|
'notes' => 'nullable|string',
|
|
]);
|
|
|
|
$validated['tenant_id'] = $this->getTenantId();
|
|
$validated['created_by'] = Auth::id();
|
|
$validated['is_paid'] = false;
|
|
|
|
// Convert jalali date if needed
|
|
if (!empty($validated['effective_date'])) {
|
|
$validated['effective_date'] = \App\Helpers\CalendarHelper::parseDate($validated['effective_date']);
|
|
}
|
|
|
|
EmployeeFinancial::create($validated);
|
|
|
|
return redirect()
|
|
->route('payroll.index', ['month' => $validated['month'], 'year' => $validated['year']])
|
|
->with('success', __('payroll.created_successfully'));
|
|
}
|
|
|
|
/**
|
|
* Show employee payroll detail.
|
|
*/
|
|
public function show(Employee $employee, Request $request)
|
|
{
|
|
$year = $request->get('year', now()->year);
|
|
|
|
$financials = EmployeeFinancial::with(['currency'])
|
|
->where('employee_id', $employee->id)
|
|
->where('year', $year)
|
|
->orderBy('month')
|
|
->orderBy('type')
|
|
->get()
|
|
->groupBy('month');
|
|
|
|
// Calculate yearly totals
|
|
$allYearFinancials = EmployeeFinancial::where('employee_id', $employee->id)
|
|
->where('year', $year)
|
|
->get();
|
|
|
|
$yearlyGross = $allYearFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount');
|
|
$yearlyDeductions = $allYearFinancials->where('type', 'deduction')->sum('amount');
|
|
$yearlyAdvances = $allYearFinancials->where('type', 'advance')->sum('amount');
|
|
$yearlyNet = $yearlyGross - $yearlyDeductions - $yearlyAdvances;
|
|
$yearlyPaid = $allYearFinancials->where('is_paid', true)->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount');
|
|
$yearlyUnpaid = $yearlyGross - $yearlyPaid;
|
|
|
|
// Work logs for the year
|
|
$workLogs = EmployeeWorkLog::where('employee_id', $employee->id)
|
|
->whereYear('date', $year)
|
|
->get()
|
|
->groupBy(function ($item) {
|
|
return \App\Helpers\CalendarHelper::formatDate($item->date, 'n');
|
|
});
|
|
|
|
$persianMonths = [
|
|
1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد',
|
|
4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور',
|
|
7 => 'مهر', 8 => 'آبان', 9 => 'آذر',
|
|
10 => 'دی', 11 => 'بهمن', 12 => 'اسفند'
|
|
];
|
|
|
|
return view('payroll.show', compact(
|
|
'employee', 'financials', 'year', 'persianMonths',
|
|
'yearlyGross', 'yearlyDeductions', 'yearlyAdvances', 'yearlyNet',
|
|
'yearlyPaid', 'yearlyUnpaid'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Mark a financial entry as paid.
|
|
*/
|
|
public function pay(EmployeeFinancial $financial)
|
|
{
|
|
$financial->update([
|
|
'is_paid' => true,
|
|
'paid_at' => now(),
|
|
]);
|
|
|
|
return redirect()->back()
|
|
->with('success', __('payroll.marked_as_paid'));
|
|
}
|
|
|
|
/**
|
|
* Bulk pay financial entries.
|
|
*/
|
|
public function payBulk(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'financial_ids' => 'required|array',
|
|
'financial_ids.*' => 'exists:employee_financials,id',
|
|
]);
|
|
|
|
$count = EmployeeFinancial::whereIn('id', $validated['financial_ids'])
|
|
->where('is_paid', false)
|
|
->update([
|
|
'is_paid' => true,
|
|
'paid_at' => now(),
|
|
]);
|
|
|
|
return redirect()->back()
|
|
->with('success', __('payroll.bulk_paid_successfully') . " ($count)");
|
|
}
|
|
|
|
/**
|
|
* Delete a financial entry.
|
|
*/
|
|
public function destroy(EmployeeFinancial $financial)
|
|
{
|
|
if ($financial->is_paid) {
|
|
return redirect()->back()
|
|
->with('error', __('payroll.cannot_delete_paid'));
|
|
}
|
|
|
|
$financial->delete();
|
|
|
|
return redirect()->back()
|
|
->with('success', __('payroll.deleted_successfully'));
|
|
}
|
|
|
|
/**
|
|
* Show settlement form.
|
|
*/
|
|
public function settlement()
|
|
{
|
|
$employees = Employee::where('tenant_id', $this->getTenantId())
|
|
->whereIn('status', ['active', 'inactive'])
|
|
->get();
|
|
$currencies = Currency::where('is_active', true)->get();
|
|
|
|
return view('payroll.settlement', compact('employees', 'currencies'));
|
|
}
|
|
|
|
/**
|
|
* Preview settlement calculation.
|
|
*/
|
|
public function previewSettlement(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'employee_id' => 'required|exists:employees,id',
|
|
'settlement_type' => 'required|in:resignation,termination,end_of_contract',
|
|
'settlement_date' => 'required|date',
|
|
'last_working_date' => 'nullable|date',
|
|
]);
|
|
|
|
$employee = Employee::findOrFail($validated['employee_id']);
|
|
$settlementDate = \App\Helpers\CalendarHelper::parseDate($validated['settlement_date']);
|
|
|
|
// Get all unpaid financials
|
|
$unpaidFinancials = EmployeeFinancial::where('employee_id', $employee->id)
|
|
->where('is_paid', false)
|
|
->with('currency')
|
|
->get();
|
|
|
|
$totalPayable = $unpaidFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount');
|
|
$totalDeductions = $unpaidFinancials->where('type', 'deduction')->sum('amount');
|
|
$totalAdvances = $unpaidFinancials->where('type', 'advance')->sum('amount');
|
|
$netPayable = $totalPayable - $totalDeductions - $totalAdvances;
|
|
|
|
// Unused vacation days calculation (rough: 2.5 days per month)
|
|
$monthsWorked = 0;
|
|
if ($employee->hire_date) {
|
|
$hireDate = \Carbon\Carbon::parse($employee->hire_date);
|
|
$settlementCarbon = \Carbon\Carbon::parse($settlementDate);
|
|
$monthsWorked = $hireDate->diffInMonths($settlementCarbon);
|
|
}
|
|
$vacationDays = min($monthsWorked * 2.5, 26); // max 26 days
|
|
|
|
return response()->json([
|
|
'employee' => $employee->only(['id', 'first_name', 'last_name', 'base_salary']),
|
|
'unpaid_financials' => $unpaidFinancials->map(function ($f) {
|
|
return [
|
|
'id' => $f->id,
|
|
'type' => $f->type,
|
|
'amount' => $f->amount,
|
|
'month' => $f->month,
|
|
'year' => $f->year,
|
|
'description' => $f->description,
|
|
];
|
|
}),
|
|
'total_payable' => $totalPayable,
|
|
'total_deductions' => $totalDeductions,
|
|
'total_advances' => $totalAdvances,
|
|
'net_payable' => $netPayable,
|
|
'months_worked' => $monthsWorked,
|
|
'vacation_days' => $vacationDays,
|
|
'financial_count' => $unpaidFinancials->count(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Process settlement.
|
|
*/
|
|
public function processSettlement(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'employee_id' => 'required|exists:employees,id',
|
|
'settlement_type' => 'required|in:resignation,termination,end_of_contract',
|
|
'settlement_date' => 'required|date',
|
|
'last_working_date' => 'nullable|date',
|
|
'notes' => 'nullable|string',
|
|
]);
|
|
|
|
$employee = Employee::findOrFail($validated['employee_id']);
|
|
$tenantId = $this->getTenantId();
|
|
$settlementDate = \App\Helpers\CalendarHelper::parseDate($validated['settlement_date']);
|
|
|
|
DB::transaction(function () use ($employee, $validated, $tenantId, $settlementDate) {
|
|
// Get all unpaid financials
|
|
$unpaidFinancials = EmployeeFinancial::where('employee_id', $employee->id)
|
|
->where('is_paid', false)
|
|
->get();
|
|
|
|
$totalPayable = $unpaidFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount');
|
|
$totalDeductions = $unpaidFinancials->where('type', 'deduction')->sum('amount');
|
|
$totalAdvances = $unpaidFinancials->where('type', 'advance')->sum('amount');
|
|
$netPayable = $totalPayable - $totalDeductions - $totalAdvances;
|
|
|
|
// Create settlement record
|
|
$settlement = \App\Models\EmployeeSettlement::create([
|
|
'tenant_id' => $tenantId,
|
|
'employee_id' => $employee->id,
|
|
'settlement_type' => $validated['settlement_type'],
|
|
'settlement_date' => $settlementDate,
|
|
'last_working_date' => !empty($validated['last_working_date'])
|
|
? \App\Helpers\CalendarHelper::parseDate($validated['last_working_date'])
|
|
: null,
|
|
'total_payable' => $totalPayable,
|
|
'total_deductions' => $totalDeductions + $totalAdvances,
|
|
'net_payable' => $netPayable,
|
|
'currency_id' => $employee->currency_id,
|
|
'notes' => $validated['notes'] ?? null,
|
|
'processed_by' => Auth::id(),
|
|
'processed_at' => now(),
|
|
]);
|
|
|
|
// Mark all unpaid as paid and link to settlement
|
|
$unpaidFinancials->each->update([
|
|
'is_paid' => true,
|
|
'paid_at' => now(),
|
|
'is_settlement' => true,
|
|
'settlement_id' => $settlement->id,
|
|
]);
|
|
|
|
// Update employee status
|
|
$employee->update([
|
|
'status' => 'terminated',
|
|
'termination_date' => $settlementDate,
|
|
]);
|
|
});
|
|
|
|
return redirect()->route('payroll.index')
|
|
->with('success', __('payroll.settlement_processed'));
|
|
}
|
|
}
|