feat:부가세 확정 신고 시 예정 세액 차감 반영
- 확정(C) 기간 조회 시 대응하는 예정(P) 기간의 netVat 자동 계산 - 예정 환급세액 → "예정신고 미환급세액"으로 차감 표시 - 예정 납부세액 → "예정신고 기납부세액"으로 차감 표시 - 최종 납부세액 = 확정 산출세액 - 예정 차감액 - 상단 요약 카드에도 최종 세액 반영 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -263,8 +263,15 @@ public function index(Request $request): JsonResponse
|
||||
'cardPurchaseSupply' => $cardPurchaseSupply,
|
||||
'cardPurchaseVat' => $cardPurchaseVat,
|
||||
'total' => $allRecords->count(),
|
||||
'preliminaryVat' => null,
|
||||
];
|
||||
|
||||
// 확정(C) 기간이면 대응하는 예정(P)의 netVat를 계산
|
||||
if ($period && str_ends_with($period, 'C')) {
|
||||
$prelimPeriod = substr($period, 0, -1) . 'P';
|
||||
$stats['preliminaryVat'] = $this->calculatePeriodNetVat($tenantId, $prelimPeriod);
|
||||
}
|
||||
|
||||
// 사용 중인 기간 목록
|
||||
$periods = VatRecord::forTenant($tenantId)
|
||||
->select('period')
|
||||
@@ -360,6 +367,99 @@ public function destroy(int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 기간의 순 부가세 계산 (매출세액 - 매입세액)
|
||||
* 양수: 납부세액, 음수: 환급세액
|
||||
*/
|
||||
private function calculatePeriodNetVat(int $tenantId, string $period): int
|
||||
{
|
||||
[$startDate, $endDate] = $this->periodToDateRange($period);
|
||||
if (!$startDate || !$endDate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$startDateYmd = str_replace('-', '', $startDate);
|
||||
$endDateYmd = str_replace('-', '', $endDate);
|
||||
|
||||
$taxTypeMap = [1 => 'taxable', 2 => 'zero_rated', 3 => 'exempt'];
|
||||
|
||||
// 홈택스 매출세액 (과세+영세)
|
||||
$salesVat = (int) HometaxInvoice::where('tenant_id', $tenantId)
|
||||
->sales()
|
||||
->period($startDate, $endDate)
|
||||
->whereIn('tax_type', [1, 2])
|
||||
->sum('tax_amount');
|
||||
|
||||
// 홈택스 매입세액 (과세+영세)
|
||||
$purchaseVat = (int) HometaxInvoice::where('tenant_id', $tenantId)
|
||||
->purchase()
|
||||
->period($startDate, $endDate)
|
||||
->whereIn('tax_type', [1, 2])
|
||||
->sum('tax_amount');
|
||||
|
||||
// 카드 매입세액 (공제분만)
|
||||
$cardVat = 0;
|
||||
$hiddenKeys = CardTransactionHide::getHiddenKeys($tenantId, $startDateYmd, $endDateYmd);
|
||||
|
||||
$cardTransactions = BarobillCardTransaction::where('tenant_id', $tenantId)
|
||||
->whereBetween('use_date', [$startDateYmd, $endDateYmd])
|
||||
->get();
|
||||
|
||||
$splitsByKey = CardTransactionSplit::getByDateRange($tenantId, $startDateYmd, $endDateYmd);
|
||||
|
||||
$splitsByPartialKey = [];
|
||||
foreach ($splitsByKey as $fullKey => $splits) {
|
||||
$parts = explode('|', $fullKey);
|
||||
if (count($parts) >= 3) {
|
||||
$partialKey = $parts[0] . '|' . $parts[1] . '|' . $parts[2];
|
||||
$splitsByPartialKey[$partialKey] = $splits;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($cardTransactions as $card) {
|
||||
if ($hiddenKeys->contains($card->unique_key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$splits = $splitsByKey[$card->unique_key] ?? null;
|
||||
if (!$splits) {
|
||||
$cardPartialKey = $card->card_num . '|' . $card->use_dt . '|' . $card->approval_num;
|
||||
$splits = $splitsByPartialKey[$cardPartialKey] ?? null;
|
||||
}
|
||||
|
||||
if ($splits && count($splits) > 0) {
|
||||
foreach ($splits as $split) {
|
||||
if ($split->deduction_type === 'deductible') {
|
||||
$cardVat += (int) $split->split_tax;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($card->deduction_type === 'deductible') {
|
||||
$cardVat += (int) ($card->modified_tax ?? $card->tax);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 수동입력 매출세액 (과세+영세)
|
||||
$manualSalesVat = (int) VatRecord::forTenant($tenantId)
|
||||
->where('period', $period)
|
||||
->where('type', 'sales')
|
||||
->whereIn('tax_type', ['taxable', 'zero_rated'])
|
||||
->sum('vat_amount');
|
||||
|
||||
// 수동입력 매입세액 (과세+영세)
|
||||
$manualPurchaseVat = (int) VatRecord::forTenant($tenantId)
|
||||
->where('period', $period)
|
||||
->where('type', 'purchase')
|
||||
->whereIn('tax_type', ['taxable', 'zero_rated'])
|
||||
->sum('vat_amount');
|
||||
|
||||
$totalSalesVat = $salesVat + $manualSalesVat;
|
||||
$totalPurchaseVat = $purchaseVat + $cardVat + $manualPurchaseVat;
|
||||
|
||||
return $totalSalesVat - $totalPurchaseVat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 부가세 신고기간을 날짜 범위로 변환
|
||||
* YYYY-1P: 1기 예정 (0101~0331)
|
||||
|
||||
@@ -183,6 +183,14 @@ function VatManagement() {
|
||||
const cardPurchaseVat = stats.cardPurchaseVat || 0;
|
||||
const netVat = salesVat - purchaseVat;
|
||||
|
||||
// 확정 기간일 때 예정 세액 차감
|
||||
const isConfirmedPeriod = filterPeriod.endsWith('C');
|
||||
const preliminaryVat = stats.preliminaryVat; // 양수=납부, 음수=환급, null=예정아님
|
||||
const hasPreliminary = isConfirmedPeriod && preliminaryVat !== null && preliminaryVat !== undefined;
|
||||
// 예정 납부세액이든 환급세액이든 절대값으로 차감 (이미 정산된 금액)
|
||||
const preliminaryDeduction = hasPreliminary ? Math.abs(preliminaryVat) : 0;
|
||||
const finalNetVat = netVat - preliminaryDeduction;
|
||||
|
||||
const handleAdd = () => { setModalMode('add'); setFormData({ ...initialFormState, period: filterPeriod }); setShowModal(true); };
|
||||
const handleEdit = (item) => {
|
||||
if (item.isCardTransaction || item.isHometax) return;
|
||||
@@ -346,9 +354,12 @@ function VatManagement() {
|
||||
<p className="text-xl font-bold text-pink-600">{formatCurrency(purchaseVat)}원</p>
|
||||
<p className="text-xs text-gray-500">공급가액: {formatCurrency(purchaseSupply)}원</p>
|
||||
</div>
|
||||
<div className={`rounded-lg p-4 ${netVat >= 0 ? 'bg-amber-50' : 'bg-blue-50'}`}>
|
||||
<div className="flex items-center gap-2 mb-1"><Calculator className={`w-4 h-4 ${netVat >= 0 ? 'text-amber-600' : 'text-blue-600'}`} /><span className={`text-sm ${netVat >= 0 ? 'text-amber-700' : 'text-blue-700'}`}>{netVat >= 0 ? '납부세액' : '환급세액'}</span></div>
|
||||
<p className={`text-xl font-bold ${netVat >= 0 ? 'text-amber-600' : 'text-blue-600'}`}>{formatCurrency(Math.abs(netVat))}원</p>
|
||||
<div className={`rounded-lg p-4 ${(hasPreliminary ? finalNetVat : netVat) >= 0 ? 'bg-amber-50' : 'bg-blue-50'}`}>
|
||||
<div className="flex items-center gap-2 mb-1"><Calculator className={`w-4 h-4 ${(hasPreliminary ? finalNetVat : netVat) >= 0 ? 'text-amber-600' : 'text-blue-600'}`} /><span className={`text-sm ${(hasPreliminary ? finalNetVat : netVat) >= 0 ? 'text-amber-700' : 'text-blue-700'}`}>{(hasPreliminary ? finalNetVat : netVat) >= 0 ? '납부세액' : '환급세액'}</span></div>
|
||||
<p className={`text-xl font-bold ${(hasPreliminary ? finalNetVat : netVat) >= 0 ? 'text-amber-600' : 'text-blue-600'}`}>{formatCurrency(Math.abs(hasPreliminary ? finalNetVat : netVat))}원</p>
|
||||
{hasPreliminary && preliminaryDeduction > 0 && (
|
||||
<p className="text-xs text-gray-500">예정 차감 전: {formatCurrency(Math.abs(netVat))}원</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<button className="flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 hover:bg-gray-50 rounded-lg">
|
||||
@@ -403,10 +414,38 @@ function VatManagement() {
|
||||
<td className="px-6 py-3 text-sm text-right text-purple-600">{formatCurrency(cardPurchaseSupply)}원</td>
|
||||
<td className="px-6 py-3 text-sm text-right text-purple-600 font-medium">({formatCurrency(cardPurchaseVat)}원)</td>
|
||||
</tr>
|
||||
{hasPreliminary && preliminaryDeduction > 0 && (
|
||||
<>
|
||||
<tr className="border-b border-gray-200 bg-gray-50/50">
|
||||
<td className="px-6 py-3 text-sm font-medium">
|
||||
{netVat >= 0 ? '납부세액(확정 자체)' : '환급세액(확정 자체)'}
|
||||
</td>
|
||||
<td className="px-6 py-3 text-sm text-right">-</td>
|
||||
<td className={`px-6 py-3 text-sm text-right font-medium ${netVat >= 0 ? 'text-amber-600' : 'text-blue-600'}`}>
|
||||
{formatCurrency(Math.abs(netVat))}원
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200 bg-indigo-50/50">
|
||||
<td className="px-6 py-3 text-sm text-indigo-700 font-medium">
|
||||
{preliminaryVat <= 0 ? '예정신고 미환급세액' : '예정신고 기납부세액'}
|
||||
</td>
|
||||
<td className="px-6 py-3 text-sm text-right">-</td>
|
||||
<td className="px-6 py-3 text-sm text-right text-indigo-600 font-medium">
|
||||
({formatCurrency(preliminaryDeduction)}원)
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
<tr className="bg-gray-50 font-bold">
|
||||
<td className="px-6 py-3 text-sm">{netVat >= 0 ? '납부세액' : '환급세액'}</td>
|
||||
<td className="px-6 py-3 text-sm">
|
||||
{hasPreliminary
|
||||
? (finalNetVat >= 0 ? '최종 납부세액' : '최종 환급세액')
|
||||
: (netVat >= 0 ? '납부세액' : '환급세액')}
|
||||
</td>
|
||||
<td className="px-6 py-3 text-sm text-right">-</td>
|
||||
<td className={`px-6 py-3 text-sm text-right ${netVat >= 0 ? 'text-amber-600' : 'text-blue-600'}`}>{formatCurrency(Math.abs(netVat))}원</td>
|
||||
<td className={`px-6 py-3 text-sm text-right ${(hasPreliminary ? finalNetVat : netVat) >= 0 ? 'text-amber-600' : 'text-blue-600'}`}>
|
||||
{formatCurrency(Math.abs(hasPreliminary ? finalNetVat : netVat))}원
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user