Files
sam-api/app/Models/Tenants/Payroll.php
김보곤 82621a6045 feat: [payroll] MNG 급여관리 계산 엔진 및 일괄 처리 API 구현
- IncomeTaxBracket 모델 추가 (2024 간이세액표 DB 조회)
- PayrollService 전면 개편: 4대보험 + 소득세 자동 계산 엔진
- 10,000천원 초과 고소득 구간 공식 계산 지원
- 과세표준 = 총지급액 - 식대(비과세), 10원 단위 절삭
- 일괄 생성(bulkGenerate), 전월 복사(copyFromPrevious) 기능
- 확정취소(unconfirm), 지급취소(unpay) 상태 관리
- 계산 미리보기(calculatePreview) 엔드포인트 추가
- 공제항목 수동 오버라이드(deduction_overrides) 지원
- Payroll 모델에 long_term_care, options 필드 추가
2026-03-11 19:18:27 +09:00

364 lines
8.6 KiB
PHP

<?php
namespace App\Models\Tenants;
use App\Models\Members\User;
use App\Traits\Auditable;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 급여 모델
*
* @property int $id
* @property int $tenant_id
* @property int $user_id
* @property int $pay_year
* @property int $pay_month
* @property float $base_salary
* @property float $overtime_pay
* @property float $bonus
* @property array|null $allowances
* @property float $gross_salary
* @property float $income_tax
* @property float $resident_tax
* @property float $health_insurance
* @property float $pension
* @property float $employment_insurance
* @property array|null $deductions
* @property float $total_deductions
* @property float $net_salary
* @property string $status
* @property \Carbon\Carbon|null $confirmed_at
* @property int|null $confirmed_by
* @property \Carbon\Carbon|null $paid_at
* @property int|null $withdrawal_id
* @property string|null $note
* @property int|null $created_by
* @property int|null $updated_by
* @property int|null $deleted_by
*/
class Payroll extends Model
{
use Auditable, BelongsToTenant, SoftDeletes;
protected $table = 'payrolls';
protected $casts = [
'allowances' => 'array',
'deductions' => 'array',
'options' => 'array',
'base_salary' => 'decimal:0',
'overtime_pay' => 'decimal:0',
'bonus' => 'decimal:0',
'gross_salary' => 'decimal:0',
'income_tax' => 'decimal:0',
'resident_tax' => 'decimal:0',
'health_insurance' => 'decimal:0',
'long_term_care' => 'decimal:0',
'pension' => 'decimal:0',
'employment_insurance' => 'decimal:0',
'total_deductions' => 'decimal:0',
'net_salary' => 'decimal:0',
'confirmed_at' => 'datetime',
'paid_at' => 'datetime',
'pay_year' => 'integer',
'pay_month' => 'integer',
];
protected $fillable = [
'tenant_id',
'user_id',
'pay_year',
'pay_month',
'base_salary',
'overtime_pay',
'bonus',
'allowances',
'gross_salary',
'income_tax',
'resident_tax',
'health_insurance',
'long_term_care',
'pension',
'employment_insurance',
'deductions',
'options',
'total_deductions',
'net_salary',
'status',
'confirmed_at',
'confirmed_by',
'paid_at',
'withdrawal_id',
'note',
'created_by',
'updated_by',
'deleted_by',
];
protected $attributes = [
'status' => 'draft',
'base_salary' => 0,
'overtime_pay' => 0,
'bonus' => 0,
'gross_salary' => 0,
'income_tax' => 0,
'resident_tax' => 0,
'health_insurance' => 0,
'long_term_care' => 0,
'pension' => 0,
'employment_insurance' => 0,
'total_deductions' => 0,
'net_salary' => 0,
];
// =========================================================================
// 상태 상수
// =========================================================================
public const STATUS_DRAFT = 'draft'; // 작성중
public const STATUS_CONFIRMED = 'confirmed'; // 확정
public const STATUS_PAID = 'paid'; // 지급완료
public const STATUSES = [
self::STATUS_DRAFT,
self::STATUS_CONFIRMED,
self::STATUS_PAID,
];
// =========================================================================
// 관계 정의
// =========================================================================
/**
* 급여 대상 사용자
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* 확정자
*/
public function confirmer(): BelongsTo
{
return $this->belongsTo(User::class, 'confirmed_by');
}
/**
* 출금 내역
*/
public function withdrawal(): BelongsTo
{
return $this->belongsTo(Withdrawal::class, 'withdrawal_id');
}
/**
* 생성자
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* 수정자
*/
public function updater(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
// =========================================================================
// 스코프
// =========================================================================
/**
* 특정 상태
*/
public function scopeWithStatus($query, string $status)
{
return $query->where('status', $status);
}
/**
* 작성중
*/
public function scopeDraft($query)
{
return $query->where('status', self::STATUS_DRAFT);
}
/**
* 확정
*/
public function scopeConfirmed($query)
{
return $query->where('status', self::STATUS_CONFIRMED);
}
/**
* 지급완료
*/
public function scopePaid($query)
{
return $query->where('status', self::STATUS_PAID);
}
/**
* 특정 연월
*/
public function scopeForPeriod($query, int $year, int $month)
{
return $query->where('pay_year', $year)->where('pay_month', $month);
}
/**
* 특정 사용자
*/
public function scopeForUser($query, int $userId)
{
return $query->where('user_id', $userId);
}
// =========================================================================
// 헬퍼 메서드
// =========================================================================
/**
* 수정 가능 여부 (작성중, 슈퍼관리자는 모든 상태)
*/
public function isEditable(bool $isSuperAdmin = false): bool
{
if ($isSuperAdmin) {
return true;
}
return $this->status === self::STATUS_DRAFT;
}
/**
* 확정 취소 가능 여부
*/
public function isUnconfirmable(): bool
{
return $this->status === self::STATUS_CONFIRMED;
}
/**
* 지급 취소 가능 여부 (슈퍼관리자 전용)
*/
public function isUnpayable(): bool
{
return $this->status === self::STATUS_PAID;
}
/**
* 확정 가능 여부
*/
public function isConfirmable(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* 지급 가능 여부
*/
public function isPayable(): bool
{
return $this->status === self::STATUS_CONFIRMED;
}
/**
* 삭제 가능 여부 (작성중만)
*/
public function isDeletable(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* 상태 라벨
*/
public function getStatusLabelAttribute(): string
{
return match ($this->status) {
self::STATUS_DRAFT => '작성중',
self::STATUS_CONFIRMED => '확정',
self::STATUS_PAID => '지급완료',
default => $this->status,
};
}
/**
* 급여 기간 문자열
*/
public function getPeriodLabelAttribute(): string
{
return sprintf('%d년 %02d월', $this->pay_year, $this->pay_month);
}
/**
* 수당 합계
*/
public function getAllowancesTotalAttribute(): float
{
if (empty($this->allowances)) {
return 0;
}
return collect($this->allowances)->sum('amount');
}
/**
* 공제 합계 (JSON)
*/
public function getDeductionsTotalAttribute(): float
{
if (empty($this->deductions)) {
return 0;
}
return collect($this->deductions)->sum('amount');
}
/**
* 총지급액 계산
*/
public function calculateGrossSalary(): float
{
return $this->base_salary
+ $this->overtime_pay
+ $this->bonus
+ $this->allowances_total;
}
/**
* 총공제액 계산
*/
public function calculateTotalDeductions(): float
{
return $this->income_tax
+ $this->resident_tax
+ $this->health_insurance
+ $this->long_term_care
+ $this->pension
+ $this->employment_insurance
+ $this->deductions_total;
}
/**
* 실수령액 계산
*/
public function calculateNetSalary(): float
{
return $this->gross_salary - $this->total_deductions;
}
}