2025-12-18 10:56:16 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models\Tenants;
|
|
|
|
|
|
|
|
|
|
use App\Models\Members\User;
|
2026-01-29 15:33:54 +09:00
|
|
|
use App\Traits\Auditable;
|
2025-12-18 10:56:16 +09:00
|
|
|
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
|
|
|
|
|
{
|
2026-01-29 15:33:54 +09:00
|
|
|
use Auditable, BelongsToTenant, SoftDeletes;
|
2025-12-18 10:56:16 +09:00
|
|
|
|
|
|
|
|
protected $table = 'payrolls';
|
|
|
|
|
|
|
|
|
|
protected $casts = [
|
|
|
|
|
'allowances' => 'array',
|
|
|
|
|
'deductions' => 'array',
|
|
|
|
|
'base_salary' => 'decimal:2',
|
|
|
|
|
'overtime_pay' => 'decimal:2',
|
|
|
|
|
'bonus' => 'decimal:2',
|
|
|
|
|
'gross_salary' => 'decimal:2',
|
|
|
|
|
'income_tax' => 'decimal:2',
|
|
|
|
|
'resident_tax' => 'decimal:2',
|
|
|
|
|
'health_insurance' => 'decimal:2',
|
|
|
|
|
'pension' => 'decimal:2',
|
|
|
|
|
'employment_insurance' => 'decimal:2',
|
|
|
|
|
'total_deductions' => 'decimal:2',
|
|
|
|
|
'net_salary' => 'decimal:2',
|
|
|
|
|
'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',
|
|
|
|
|
'pension',
|
|
|
|
|
'employment_insurance',
|
|
|
|
|
'deductions',
|
|
|
|
|
'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,
|
|
|
|
|
'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
|
|
|
|
|
{
|
|
|
|
|
return $this->status === self::STATUS_DRAFT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 확정 가능 여부
|
|
|
|
|
*/
|
|
|
|
|
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->pension
|
|
|
|
|
+ $this->employment_insurance
|
|
|
|
|
+ $this->deductions_total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 실수령액 계산
|
|
|
|
|
*/
|
|
|
|
|
public function calculateNetSalary(): float
|
|
|
|
|
{
|
|
|
|
|
return $this->gross_salary - $this->total_deductions;
|
|
|
|
|
}
|
|
|
|
|
}
|