feat: [payroll] 엑셀 내보내기 및 전표 생성 API 추가
- GET /payrolls/export: 급여 현황 엑셀 다운로드 (필터 지원) - POST /payrolls/journal-entries: 연월 기준 급여 전표 일괄 생성 - JournalEntry SOURCE_PAYROLL 상수 추가 - StorePayrollJournalRequest 유효성 검증 추가
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\IncomeTaxBracket;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Models\Tenants\Payroll;
|
||||
use App\Models\Tenants\PayrollSetting;
|
||||
use App\Models\Tenants\TenantUserProfile;
|
||||
@@ -916,4 +917,258 @@ public function resolveFamilyCount(int $userId): int
|
||||
|
||||
return max(1, min(11, 1 + $dependentCount));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 엑셀 내보내기
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 급여 엑셀 내보내기용 데이터
|
||||
*/
|
||||
public function getExportData(array $params): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Payroll::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['user:id,name,email', 'user.tenantProfiles' => function ($q) use ($tenantId) {
|
||||
$q->where('tenant_id', $tenantId)->with('department:id,name');
|
||||
}]);
|
||||
|
||||
if (! empty($params['year'])) {
|
||||
$query->where('pay_year', $params['year']);
|
||||
}
|
||||
if (! empty($params['month'])) {
|
||||
$query->where('pay_month', $params['month']);
|
||||
}
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
if (! empty($params['user_id'])) {
|
||||
$query->where('user_id', $params['user_id']);
|
||||
}
|
||||
if (! empty($params['department_id'])) {
|
||||
$deptId = $params['department_id'];
|
||||
$query->whereHas('user.tenantProfiles', function ($q) use ($tenantId, $deptId) {
|
||||
$q->where('tenant_id', $tenantId)->where('department_id', $deptId);
|
||||
});
|
||||
}
|
||||
if (! empty($params['search'])) {
|
||||
$query->whereHas('user', function ($q) use ($params) {
|
||||
$q->where('name', 'like', "%{$params['search']}%");
|
||||
});
|
||||
}
|
||||
|
||||
$sortBy = $params['sort_by'] ?? 'pay_year';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
|
||||
if ($sortBy === 'period') {
|
||||
$query->orderBy('pay_year', $sortDir)->orderBy('pay_month', $sortDir);
|
||||
} else {
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
}
|
||||
|
||||
$payrolls = $query->get();
|
||||
|
||||
$statusLabels = [
|
||||
Payroll::STATUS_DRAFT => '작성중',
|
||||
Payroll::STATUS_CONFIRMED => '확정',
|
||||
Payroll::STATUS_PAID => '지급완료',
|
||||
];
|
||||
|
||||
$data = $payrolls->map(function ($payroll) use ($statusLabels) {
|
||||
$profile = $payroll->user?->tenantProfiles?->first();
|
||||
$department = $profile?->department?->name ?? '-';
|
||||
|
||||
return [
|
||||
$payroll->pay_year.'년 '.$payroll->pay_month.'월',
|
||||
$payroll->user?->name ?? '-',
|
||||
$department,
|
||||
number_format($payroll->base_salary),
|
||||
number_format($payroll->overtime_pay),
|
||||
number_format($payroll->bonus),
|
||||
number_format($payroll->gross_salary),
|
||||
number_format($payroll->income_tax),
|
||||
number_format($payroll->resident_tax),
|
||||
number_format($payroll->health_insurance),
|
||||
number_format($payroll->long_term_care),
|
||||
number_format($payroll->pension),
|
||||
number_format($payroll->employment_insurance),
|
||||
number_format($payroll->total_deductions),
|
||||
number_format($payroll->net_salary),
|
||||
$statusLabels[$payroll->status] ?? $payroll->status,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$headings = [
|
||||
'급여월',
|
||||
'직원명',
|
||||
'부서',
|
||||
'기본급',
|
||||
'야근수당',
|
||||
'상여금',
|
||||
'총지급액',
|
||||
'소득세',
|
||||
'주민세',
|
||||
'건강보험',
|
||||
'장기요양',
|
||||
'국민연금',
|
||||
'고용보험',
|
||||
'공제합계',
|
||||
'실지급액',
|
||||
'상태',
|
||||
];
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'headings' => $headings,
|
||||
];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 전표 생성
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 급여 전표 일괄 생성
|
||||
*
|
||||
* 해당 연월의 확정/지급완료 급여를 합산하여 전표를 생성한다.
|
||||
* - 차변: 급여 (총지급액)
|
||||
* - 대변: 각 공제항목 + 미지급금(실지급액)
|
||||
*/
|
||||
public function createJournalEntries(int $year, int $month, ?string $entryDate = null): JournalEntry
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$payrolls = Payroll::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->forPeriod($year, $month)
|
||||
->whereIn('status', [Payroll::STATUS_CONFIRMED, Payroll::STATUS_PAID])
|
||||
->get();
|
||||
|
||||
if ($payrolls->isEmpty()) {
|
||||
throw new BadRequestHttpException(__('error.payroll.no_confirmed_payrolls'));
|
||||
}
|
||||
|
||||
// 합산
|
||||
$totalGross = $payrolls->sum('gross_salary');
|
||||
$totalIncomeTax = $payrolls->sum('income_tax');
|
||||
$totalResidentTax = $payrolls->sum('resident_tax');
|
||||
$totalHealthInsurance = $payrolls->sum('health_insurance');
|
||||
$totalLongTermCare = $payrolls->sum('long_term_care');
|
||||
$totalPension = $payrolls->sum('pension');
|
||||
$totalEmploymentInsurance = $payrolls->sum('employment_insurance');
|
||||
$totalNet = $payrolls->sum('net_salary');
|
||||
|
||||
// 전표일자: 지정값 또는 해당월 급여지급일
|
||||
if (! $entryDate) {
|
||||
$settings = PayrollSetting::getOrCreate($tenantId);
|
||||
$payDay = min($settings->pay_day, 28);
|
||||
$entryDate = sprintf('%04d-%02d-%02d', $year, $month, $payDay);
|
||||
}
|
||||
|
||||
$sourceKey = "payroll_{$year}_{$month}";
|
||||
$description = "{$year}년 {$month}월 급여";
|
||||
|
||||
// 분개 행 구성
|
||||
$rows = [];
|
||||
|
||||
// 차변: 급여 (총지급액)
|
||||
$rows[] = [
|
||||
'side' => 'debit',
|
||||
'account_code' => '51100',
|
||||
'account_name' => '급여',
|
||||
'debit_amount' => (int) $totalGross,
|
||||
'credit_amount' => 0,
|
||||
'memo' => $description,
|
||||
];
|
||||
|
||||
// 대변: 소득세예수금
|
||||
if ($totalIncomeTax > 0) {
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25500',
|
||||
'account_name' => '예수금-소득세',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => (int) $totalIncomeTax,
|
||||
];
|
||||
}
|
||||
|
||||
// 대변: 주민세예수금
|
||||
if ($totalResidentTax > 0) {
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25501',
|
||||
'account_name' => '예수금-주민세',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => (int) $totalResidentTax,
|
||||
];
|
||||
}
|
||||
|
||||
// 대변: 건강보험예수금
|
||||
if ($totalHealthInsurance > 0) {
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25502',
|
||||
'account_name' => '예수금-건강보험',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => (int) $totalHealthInsurance,
|
||||
];
|
||||
}
|
||||
|
||||
// 대변: 장기요양보험예수금
|
||||
if ($totalLongTermCare > 0) {
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25503',
|
||||
'account_name' => '예수금-장기요양',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => (int) $totalLongTermCare,
|
||||
];
|
||||
}
|
||||
|
||||
// 대변: 국민연금예수금
|
||||
if ($totalPension > 0) {
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25504',
|
||||
'account_name' => '예수금-국민연금',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => (int) $totalPension,
|
||||
];
|
||||
}
|
||||
|
||||
// 대변: 고용보험예수금
|
||||
if ($totalEmploymentInsurance > 0) {
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25505',
|
||||
'account_name' => '예수금-고용보험',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => (int) $totalEmploymentInsurance,
|
||||
];
|
||||
}
|
||||
|
||||
// 대변: 미지급금 (실지급액)
|
||||
if ($totalNet > 0) {
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25300',
|
||||
'account_name' => '미지급금',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => (int) $totalNet,
|
||||
'memo' => "급여 실지급액 ({$payrolls->count()}명)",
|
||||
];
|
||||
}
|
||||
|
||||
$syncService = app(JournalSyncService::class);
|
||||
|
||||
return $syncService->saveForSource(
|
||||
JournalEntry::SOURCE_PAYROLL,
|
||||
$sourceKey,
|
||||
$entryDate,
|
||||
$description,
|
||||
$rows
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user