fix: [payroll] 전표 생성 시 기타공제 누락 수정 및 에러 모달 추가
- 기타공제(deductions JSON) 항목을 대변 207 예수금에 반영 - 차대 불균형 시 상세 분개 내역을 에러 모달로 표시 - toast 대신 복사 가능한 모달로 에러 메시지 표시
This commit is contained in:
@@ -738,27 +738,44 @@ public function generateJournalEntry(Request $request): JsonResponse
|
||||
}
|
||||
|
||||
// 해당월 급여 합산
|
||||
$sums = Payroll::forTenant($tenantId)
|
||||
$payrolls = Payroll::forTenant($tenantId)
|
||||
->forPeriod($year, $month)
|
||||
->selectRaw('
|
||||
SUM(gross_salary) as total_gross,
|
||||
SUM(pension) as total_pension,
|
||||
SUM(health_insurance) as total_health,
|
||||
SUM(long_term_care) as total_ltc,
|
||||
SUM(employment_insurance) as total_emp,
|
||||
SUM(income_tax) as total_income_tax,
|
||||
SUM(resident_tax) as total_resident_tax,
|
||||
SUM(net_salary) as total_net
|
||||
')
|
||||
->first();
|
||||
->get();
|
||||
|
||||
if (! $sums || (int) $sums->total_gross === 0) {
|
||||
if ($payrolls->isEmpty()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '해당 월 급여 데이터가 없습니다.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 기타공제(deductions JSON) 합산 포함
|
||||
$extraDeductionsTotal = 0;
|
||||
foreach ($payrolls as $p) {
|
||||
foreach ($p->deductions ?? [] as $d) {
|
||||
$extraDeductionsTotal += (int) ($d['amount'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
$sums = (object) [
|
||||
'total_gross' => $payrolls->sum('gross_salary'),
|
||||
'total_pension' => $payrolls->sum('pension'),
|
||||
'total_health' => $payrolls->sum('health_insurance'),
|
||||
'total_ltc' => $payrolls->sum('long_term_care'),
|
||||
'total_emp' => $payrolls->sum('employment_insurance'),
|
||||
'total_income_tax' => $payrolls->sum('income_tax'),
|
||||
'total_resident_tax' => $payrolls->sum('resident_tax'),
|
||||
'total_net' => $payrolls->sum('net_salary'),
|
||||
'total_extra_deductions' => $extraDeductionsTotal,
|
||||
];
|
||||
|
||||
if ((int) $sums->total_gross === 0) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '해당 월 급여 데이터의 총지급액이 0입니다.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 거래처 조회
|
||||
$partnerNames = ['임직원', '건강보험연금', '건강보험건강', '건강보험고용', '강서세무서', '강서구청'];
|
||||
$partners = TradingPartner::forTenant($tenantId)
|
||||
@@ -906,7 +923,23 @@ public function generateJournalEntry(Request $request): JsonResponse
|
||||
];
|
||||
}
|
||||
|
||||
// 8. 대변: 205 미지급비용 / 임직원 — 급여
|
||||
// 8. 대변: 207 예수금 / 임직원 — 기타공제
|
||||
$extraDeductions = (int) $sums->total_extra_deductions;
|
||||
if ($extraDeductions > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'credit',
|
||||
'account_code' => '207',
|
||||
'account_name' => $accountCodes['207'],
|
||||
'trading_partner_id' => $partners['임직원'],
|
||||
'trading_partner_name' => '임직원',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $extraDeductions,
|
||||
'description' => '기타공제',
|
||||
'line_no' => $lineNo++,
|
||||
];
|
||||
}
|
||||
|
||||
// 9. 대변: 205 미지급비용 / 임직원 — 급여
|
||||
$netSalary = (int) $sums->total_net;
|
||||
if ($netSalary > 0) {
|
||||
$lines[] = [
|
||||
@@ -927,9 +960,18 @@ public function generateJournalEntry(Request $request): JsonResponse
|
||||
$totalCredit = collect($lines)->sum('credit_amount');
|
||||
|
||||
if ($totalDebit !== $totalCredit || $totalDebit === 0) {
|
||||
$detail = collect($lines)->map(fn ($l) => sprintf(
|
||||
'[%s] %s %s: %s',
|
||||
$l['dc_type'],
|
||||
$l['account_code'],
|
||||
$l['description'],
|
||||
number_format($l['dc_type'] === 'debit' ? $l['debit_amount'] : $l['credit_amount'])
|
||||
))->implode("\n");
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => "차변({$totalDebit})과 대변({$totalCredit})이 일치하지 않습니다.",
|
||||
'message' => '차변('.number_format($totalDebit).')과 대변('.number_format($totalCredit).")이 일치하지 않습니다.\n차이: ".number_format(abs($totalDebit - $totalCredit)),
|
||||
'detail' => "급여 {$payrolls->count()}건 합산\n\n{$detail}",
|
||||
], 422);
|
||||
}
|
||||
|
||||
|
||||
@@ -841,6 +841,31 @@ class="px-5 py-2 text-sm text-white bg-violet-600 hover:bg-violet-700 rounded-lg
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- 에러 상세 모달 --}}
|
||||
<div id="errorDetailModal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="fixed inset-0 bg-black/50" onclick="closeErrorModal()"></div>
|
||||
<div class="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl shadow-2xl w-full max-w-lg max-h-[80vh] flex flex-col" onclick="event.stopPropagation()">
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-bold text-red-600">오류 발생</h3>
|
||||
<button onclick="closeErrorModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-6 py-4 overflow-y-auto flex-1">
|
||||
<p id="errorDetailMessage" class="text-sm text-gray-800 font-medium whitespace-pre-line mb-3"></p>
|
||||
<pre id="errorDetailContent" class="text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded-lg p-3 whitespace-pre-wrap hidden"></pre>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2 px-6 py-3 border-t border-gray-200 bg-gray-50 rounded-b-xl">
|
||||
<button onclick="copyErrorDetail()" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||
복사
|
||||
</button>
|
||||
<button onclick="closeErrorModal()" class="px-3 py-1.5 text-sm font-medium text-white bg-gray-600 rounded-lg hover:bg-gray-700">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
@@ -941,12 +966,12 @@ function generateJournalEntry() {
|
||||
if (result.success) {
|
||||
showToast(result.message, 'success');
|
||||
} else {
|
||||
showToast(result.message, 'error');
|
||||
showErrorModal(result.message, result.detail || null);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('전표 생성 중 오류가 발생했습니다.', 'error');
|
||||
showErrorModal('전표 생성 중 오류가 발생했습니다.', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1780,6 +1805,32 @@ function printPayslipPreview() {
|
||||
win.print();
|
||||
}
|
||||
|
||||
// ===== 에러 모달 =====
|
||||
function showErrorModal(message, detail) {
|
||||
document.getElementById('errorDetailMessage').textContent = message;
|
||||
const contentEl = document.getElementById('errorDetailContent');
|
||||
if (detail) {
|
||||
contentEl.textContent = detail;
|
||||
contentEl.classList.remove('hidden');
|
||||
} else {
|
||||
contentEl.classList.add('hidden');
|
||||
}
|
||||
document.getElementById('errorDetailModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeErrorModal() {
|
||||
document.getElementById('errorDetailModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function copyErrorDetail() {
|
||||
const message = document.getElementById('errorDetailMessage').textContent;
|
||||
const detail = document.getElementById('errorDetailContent').textContent;
|
||||
const text = detail ? message + '\n\n' + detail : message;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast('클립보드에 복사되었습니다.', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 토스트 메시지 =====
|
||||
function showToast(message, type) {
|
||||
if (typeof window.showToastMessage === 'function') {
|
||||
|
||||
Reference in New Issue
Block a user