diff --git a/app/Http/Controllers/Finance/PayableController.php b/app/Http/Controllers/Finance/PayableController.php index f8dac82d..c7d6fa3b 100644 --- a/app/Http/Controllers/Finance/PayableController.php +++ b/app/Http/Controllers/Finance/PayableController.php @@ -17,8 +17,7 @@ public function index(Request $request): JsonResponse if ($search = $request->input('search')) { $query->where(function ($q) use ($search) { - $q->where('vendor_name', 'like', "%{$search}%") - ->orWhere('invoice_no', 'like', "%{$search}%"); + $q->where('vendor_name', 'like', "%{$search}%"); }); } @@ -49,6 +48,7 @@ public function index(Request $request): JsonResponse 'status' => $item->status, 'description' => $item->description, 'memo' => $item->memo, + 'taxInvoiceIssued' => (bool) $item->tax_invoice_issued, ]; }); @@ -79,7 +79,7 @@ public function store(Request $request): JsonResponse { $request->validate([ 'vendorName' => 'required|string|max:100', - 'invoiceNo' => 'required|string|max:50', + 'invoiceNo' => 'nullable|string|max:50', 'amount' => 'required|integer|min:0', ]); @@ -97,6 +97,7 @@ public function store(Request $request): JsonResponse 'status' => 'unpaid', 'description' => $request->input('description'), 'memo' => $request->input('memo'), + 'tax_invoice_issued' => $request->boolean('taxInvoiceIssued', false), ]); return response()->json([ @@ -112,7 +113,7 @@ public function update(Request $request, int $id): JsonResponse $request->validate([ 'vendorName' => 'required|string|max:100', - 'invoiceNo' => 'required|string|max:50', + 'invoiceNo' => 'nullable|string|max:50', 'amount' => 'required|integer|min:0', ]); @@ -126,6 +127,7 @@ public function update(Request $request, int $id): JsonResponse 'status' => $request->input('status', $payable->status), 'description' => $request->input('description'), 'memo' => $request->input('memo'), + 'tax_invoice_issued' => $request->boolean('taxInvoiceIssued', $payable->tax_invoice_issued), ]); return response()->json([ diff --git a/app/Http/Controllers/Finance/SalesRecordController.php b/app/Http/Controllers/Finance/SalesRecordController.php index 2df87bbe..4f00b582 100644 --- a/app/Http/Controllers/Finance/SalesRecordController.php +++ b/app/Http/Controllers/Finance/SalesRecordController.php @@ -30,8 +30,8 @@ public function index(Request $request): JsonResponse $records = $query->orderBy('date', 'desc')->get()->map(fn($item) => [ 'id' => $item->id, 'date' => $item->date?->format('Y-m-d'), 'customer' => $item->customer, 'project' => $item->project, - 'type' => $item->type, 'amount' => $item->amount, - 'vat' => $item->vat, 'status' => $item->status, + 'type' => $item->type, 'taxType' => $item->tax_type ?? 'taxable', + 'amount' => $item->amount, 'vat' => $item->vat, 'status' => $item->status, 'invoiceNo' => $item->invoice_no, 'memo' => $item->memo, ]); @@ -48,14 +48,18 @@ public function index(Request $request): JsonResponse public function store(Request $request): JsonResponse { - $request->validate(['customer' => 'required|string|max:100', 'amount' => 'required|integer|min:0']); + $request->validate([ + 'customer' => 'required|string|max:100', + 'amount' => 'required|integer|min:0', + 'taxType' => 'nullable|in:taxable,zero_rated,exempt', + ]); $tenantId = session('selected_tenant_id', 1); SalesRecord::create([ 'tenant_id' => $tenantId, 'date' => $request->input('date'), 'customer' => $request->input('customer'), 'project' => $request->input('project'), - 'type' => $request->input('type'), 'amount' => $request->input('amount', 0), - 'vat' => $request->input('vat', 0), + 'type' => $request->input('type'), 'tax_type' => $request->input('taxType', 'taxable'), + 'amount' => $request->input('amount', 0), 'vat' => $request->input('vat', 0), 'status' => $request->input('status', 'contracted'), 'invoice_no' => $request->input('invoiceNo'), 'memo' => $request->input('memo'), ]); @@ -67,11 +71,16 @@ public function update(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $item = SalesRecord::forTenant($tenantId)->findOrFail($id); - $request->validate(['customer' => 'required|string|max:100', 'amount' => 'required|integer|min:0']); + $request->validate([ + 'customer' => 'required|string|max:100', + 'amount' => 'required|integer|min:0', + 'taxType' => 'nullable|in:taxable,zero_rated,exempt', + ]); $item->update([ 'date' => $request->input('date'), 'customer' => $request->input('customer'), 'project' => $request->input('project'), 'type' => $request->input('type', $item->type), + 'tax_type' => $request->input('taxType', $item->tax_type), 'amount' => $request->input('amount'), 'vat' => $request->input('vat', $item->vat), 'status' => $request->input('status', $item->status), 'invoice_no' => $request->input('invoiceNo'), 'memo' => $request->input('memo'), diff --git a/app/Http/Controllers/Finance/VatRecordController.php b/app/Http/Controllers/Finance/VatRecordController.php index 7754516e..bfdd2c3b 100644 --- a/app/Http/Controllers/Finance/VatRecordController.php +++ b/app/Http/Controllers/Finance/VatRecordController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\Finance\VatRecord; +use App\Models\Finance\CardTransaction; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -17,8 +18,7 @@ public function index(Request $request): JsonResponse if ($search = $request->input('search')) { $query->where(function ($q) use ($search) { - $q->where('partner_name', 'like', "%{$search}%") - ->orWhere('invoice_no', 'like', "%{$search}%"); + $q->where('partner_name', 'like', "%{$search}%"); }); } @@ -32,6 +32,12 @@ public function index(Request $request): JsonResponse } } + if ($taxType = $request->input('tax_type')) { + if ($taxType !== 'all') { + $query->where('tax_type', $taxType); + } + } + if ($status = $request->input('status')) { if ($status !== 'all') { $query->where('status', $status); @@ -45,6 +51,7 @@ public function index(Request $request): JsonResponse 'id' => $record->id, 'period' => $record->period, 'type' => $record->type, + 'taxType' => $record->tax_type ?? 'taxable', 'partnerName' => $record->partner_name, 'invoiceNo' => $record->invoice_no, 'invoiceDate' => $record->invoice_date?->format('Y-m-d'), @@ -53,9 +60,53 @@ public function index(Request $request): JsonResponse 'totalAmount' => $record->total_amount, 'status' => $record->status, 'memo' => $record->memo, + 'isCardTransaction' => false, ]; }); + // 카드 공제분 매입 반영 + $cardRecords = collect(); + $cardPurchaseSupply = 0; + $cardPurchaseVat = 0; + + if ($period = $request->input('period')) { + [$startDate, $endDate] = $this->periodToDateRange($period); + + if ($startDate && $endDate) { + $cardQuery = CardTransaction::forTenant($tenantId) + ->where('deduction_type', 'deductible') + ->whereBetween('transaction_date', [$startDate, $endDate]); + + $cardTransactions = $cardQuery->orderBy('transaction_date', 'desc')->get(); + + $cardRecords = $cardTransactions->map(function ($card) use ($period) { + $supply = (int) round($card->amount / 1.1); + $vat = $card->amount - $supply; + return [ + 'id' => 'card_' . $card->id, + 'period' => $period, + 'type' => 'purchase', + 'taxType' => 'taxable', + 'partnerName' => $card->merchant, + 'invoiceNo' => $card->approval_no ?? '', + 'invoiceDate' => $card->transaction_date?->format('Y-m-d'), + 'supplyAmount' => $supply, + 'vatAmount' => $vat, + 'totalAmount' => $card->amount, + 'status' => 'filed', + 'memo' => $card->memo, + 'isCardTransaction' => true, + ]; + }); + + $cardPurchaseSupply = $cardRecords->sum('supplyAmount'); + $cardPurchaseVat = $cardRecords->sum('vatAmount'); + } + } + + // 통합 records + $allRecords = $records->concat($cardRecords)->values(); + // 해당 기간 통계 $periodQuery = VatRecord::forTenant($tenantId); if ($period = $request->input('period')) { @@ -66,8 +117,10 @@ public function index(Request $request): JsonResponse $stats = [ 'salesSupply' => $periodRecords->where('type', 'sales')->sum('supply_amount'), 'salesVat' => $periodRecords->where('type', 'sales')->sum('vat_amount'), - 'purchaseSupply' => $periodRecords->where('type', 'purchase')->sum('supply_amount'), - 'purchaseVat' => $periodRecords->where('type', 'purchase')->sum('vat_amount'), + 'purchaseSupply' => $periodRecords->where('type', 'purchase')->sum('supply_amount') + $cardPurchaseSupply, + 'purchaseVat' => $periodRecords->where('type', 'purchase')->sum('vat_amount') + $cardPurchaseVat, + 'cardPurchaseSupply' => $cardPurchaseSupply, + 'cardPurchaseVat' => $cardPurchaseVat, 'total' => $periodRecords->count(), ]; @@ -81,7 +134,7 @@ public function index(Request $request): JsonResponse return response()->json([ 'success' => true, - 'data' => $records, + 'data' => $allRecords, 'stats' => $stats, 'periods' => $periods, ]); @@ -91,9 +144,9 @@ public function store(Request $request): JsonResponse { $request->validate([ 'partnerName' => 'required|string|max:100', - 'invoiceNo' => 'required|string|max:50', 'period' => 'required|string|max:20', 'type' => 'required|in:sales,purchase', + 'taxType' => 'nullable|in:taxable,zero_rated,exempt', 'supplyAmount' => 'required|integer|min:0', ]); @@ -103,6 +156,7 @@ public function store(Request $request): JsonResponse 'tenant_id' => $tenantId, 'period' => $request->input('period'), 'type' => $request->input('type', 'sales'), + 'tax_type' => $request->input('taxType', 'taxable'), 'partner_name' => $request->input('partnerName'), 'invoice_no' => $request->input('invoiceNo'), 'invoice_date' => $request->input('invoiceDate'), @@ -127,15 +181,16 @@ public function update(Request $request, int $id): JsonResponse $request->validate([ 'partnerName' => 'required|string|max:100', - 'invoiceNo' => 'required|string|max:50', 'period' => 'required|string|max:20', 'type' => 'required|in:sales,purchase', + 'taxType' => 'nullable|in:taxable,zero_rated,exempt', 'supplyAmount' => 'required|integer|min:0', ]); $record->update([ 'period' => $request->input('period'), 'type' => $request->input('type'), + 'tax_type' => $request->input('taxType', $record->tax_type), 'partner_name' => $request->input('partnerName'), 'invoice_no' => $request->input('invoiceNo'), 'invoice_date' => $request->input('invoiceDate'), @@ -163,4 +218,31 @@ public function destroy(int $id): JsonResponse 'message' => '세금계산서가 삭제되었습니다.', ]); } + + /** + * 부가세 신고기간을 날짜 범위로 변환 + * YYYY-1P: 1기 예정 (0101~0331) + * YYYY-1C: 1기 확정 (0401~0630) + * YYYY-2P: 2기 예정 (0701~0930) + * YYYY-2C: 2기 확정 (1001~1231) + */ + private function periodToDateRange(string $period): array + { + $parts = explode('-', $period); + if (count($parts) !== 2) { + return [null, null]; + } + + $year = $parts[0]; + $code = $parts[1]; + + $ranges = [ + '1P' => ["{$year}-01-01", "{$year}-03-31"], + '1C' => ["{$year}-04-01", "{$year}-06-30"], + '2P' => ["{$year}-07-01", "{$year}-09-30"], + '2C' => ["{$year}-10-01", "{$year}-12-31"], + ]; + + return $ranges[$code] ?? [null, null]; + } } diff --git a/app/Models/Finance/Payable.php b/app/Models/Finance/Payable.php index 6338f334..435012a9 100644 --- a/app/Models/Finance/Payable.php +++ b/app/Models/Finance/Payable.php @@ -23,6 +23,7 @@ class Payable extends Model 'status', 'description', 'memo', + 'tax_invoice_issued', ]; protected $casts = [ @@ -30,6 +31,7 @@ class Payable extends Model 'paid_amount' => 'integer', 'issue_date' => 'date', 'due_date' => 'date', + 'tax_invoice_issued' => 'boolean', ]; public function scopeForTenant($query, $tenantId) diff --git a/app/Models/Finance/SalesRecord.php b/app/Models/Finance/SalesRecord.php index a7cb0f9e..ba0f44d3 100644 --- a/app/Models/Finance/SalesRecord.php +++ b/app/Models/Finance/SalesRecord.php @@ -11,7 +11,7 @@ class SalesRecord extends Model protected $table = 'sales_records'; protected $fillable = [ - 'tenant_id', 'date', 'customer', 'project', 'type', + 'tenant_id', 'date', 'customer', 'project', 'type', 'tax_type', 'amount', 'vat', 'status', 'invoice_no', 'memo', ]; diff --git a/app/Models/Finance/VatRecord.php b/app/Models/Finance/VatRecord.php index b648ff58..8eb08839 100644 --- a/app/Models/Finance/VatRecord.php +++ b/app/Models/Finance/VatRecord.php @@ -15,6 +15,7 @@ class VatRecord extends Model 'tenant_id', 'period', 'type', + 'tax_type', 'partner_name', 'invoice_no', 'invoice_date', diff --git a/resources/views/finance/payables.blade.php b/resources/views/finance/payables.blade.php index 7c0c71bf..ba6b7243 100644 --- a/resources/views/finance/payables.blade.php +++ b/resources/views/finance/payables.blade.php @@ -81,7 +81,8 @@ function PayablesManagement() { status: 'unpaid', category: '사무용품', description: '', - memo: '' + memo: '', + taxInvoiceIssued: false }; const [formData, setFormData] = useState(initialFormState); @@ -120,8 +121,7 @@ function PayablesManagement() { useEffect(() => { fetchPayables(); }, []); const filteredPayables = payables.filter(item => { - const matchesSearch = (item.vendorName || '').toLowerCase().includes(searchTerm.toLowerCase()) || - (item.invoiceNo || '').toLowerCase().includes(searchTerm.toLowerCase()); + const matchesSearch = (item.vendorName || '').toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = filterStatus === 'all' || item.status === filterStatus; const matchesCategory = filterCategory === 'all' || item.category === filterCategory; return matchesSearch && matchesStatus && matchesCategory; @@ -132,12 +132,12 @@ function PayablesManagement() { setModalMode('edit'); setEditingItem(item); const safeItem = {}; - Object.keys(initialFormState).forEach(key => { safeItem[key] = item[key] ?? ''; }); + Object.keys(initialFormState).forEach(key => { safeItem[key] = item[key] ?? (key === 'taxInvoiceIssued' ? false : ''); }); setFormData(safeItem); setShowModal(true); }; const handleSave = async () => { - if (!formData.vendorName || !formData.invoiceNo || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; } + if (!formData.vendorName || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; } setSaving(true); try { const url = modalMode === 'add' ? '/finance/payables/store' : `/finance/payables/${editingItem.id}`; @@ -214,8 +214,8 @@ function PayablesManagement() { }; const handleDownload = () => { - const rows = [['미지급금 관리'], [], ['거래처', '청구서번호', '발행일', '만기일', '분류', '청구금액', '지급액', '잔액', '상태'], - ...filteredPayables.map(item => [item.vendorName, item.invoiceNo, item.issueDate, item.dueDate, item.category, item.amount, item.paidAmount, item.amount - item.paidAmount, getStatusLabel(item.status)])]; + const rows = [['미지급금 관리'], [], ['거래처', '거래일자', '결제예정일', '분류', '청구금액', '지급액', '잔액', '상태', '세금계산서'], + ...filteredPayables.map(item => [item.vendorName, item.issueDate, item.dueDate, item.category, item.amount, item.paidAmount, item.amount - item.paidAmount, getStatusLabel(item.status), item.taxInvoiceIssued ? 'O' : 'X'])]; const csvContent = rows.map(row => row.join(',')).join('\n'); const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `미지급금관리_${new Date().toISOString().split('T')[0]}.csv`; link.click(); @@ -275,7 +275,7 @@ function PayablesManagement() {
| 날짜 | +작성일자 | 거래처 | 프로젝트 | 유형 | +구분 | 공급가액 | 합계(VAT포함) | 상태 | @@ -255,20 +271,21 @@ function SalesManagement() {|||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| + | |||||||||||||||||||||||||
|
데이터를 불러오는 중...
| |||||||||||||||||||||||||
| 데이터가 없습니다. | |||||||||||||||||||||||||
| 데이터가 없습니다. | |||||||||||||||||||||||||
| {item.date} | {item.customer} |
{item.project} {item.memo &&{item.memo} } |
{item.type} | +{getTaxTypeLabel(item.taxType)} | {formatCurrency(item.amount)}원 | {formatCurrency(item.amount + item.vat)}원 | {getStatusLabel(item.status)} | @@ -291,7 +308,7 @@ function SalesManagement() {||||||||||||||||||
| 매입 | -{formatCurrency(purchaseSupply)}원 | -({formatCurrency(purchaseVat)}원) | +{formatCurrency(purchaseSupply - cardPurchaseSupply)}원 | +({formatCurrency(purchaseVat - cardPurchaseVat)}원) | +|||||||||||||||||||||
| 매입(카드) | +{formatCurrency(cardPurchaseSupply)}원 | +({formatCurrency(cardPurchaseVat)}원) | |||||||||||||||||||||||
| {netVat >= 0 ? '납부세액' : '환급세액'} | @@ -331,15 +375,22 @@ function VatManagement() {|||||||||||||||||||||||||
| 구분 | +세금구분 | 거래처 | -세금계산서번호 | -발행일 | +작성일자 | 공급가액 | 부가세 | 합계 | @@ -374,10 +425,10 @@ function VatManagement() { ) : filteredRecords.length === 0 ? (|||||||||||||||||
| 데이터가 없습니다. | |||||||||||||||||||||||||
| {getTypeLabel(item.type)} | +|||||||||||||||||||||||||
| {getTypeLabel(item.type, item.isCardTransaction)} | +{getTaxTypeLabel(item.taxType)} | {item.partnerName} | -{item.invoiceNo} | {item.invoiceDate} | {formatCurrency(item.supplyAmount)}원 | {formatCurrency(item.vatAmount)}원 | @@ -397,16 +448,17 @@ function VatManagement() {|||||||||||||||||||