input('search')) { $query->where(function ($q) use ($search) { $q->where('vendor_name', 'like', "%{$search}%"); }); } if ($status = $request->input('status')) { if ($status !== 'all') { $query->where('status', $status); } } if ($category = $request->input('category')) { if ($category !== 'all') { $query->where('category', $category); } } $payables = $query->orderBy('created_at', 'desc') ->get() ->map(function ($item) { return [ 'id' => $item->id, 'vendorName' => $item->vendor_name, 'invoiceNo' => $item->invoice_no, 'issueDate' => $item->issue_date?->format('Y-m-d'), 'dueDate' => $item->due_date?->format('Y-m-d'), 'category' => $item->category, 'amount' => $item->amount, 'paidAmount' => $item->paid_amount, 'status' => $item->status, 'description' => $item->description, 'memo' => $item->memo, 'taxInvoiceIssued' => (bool) $item->tax_invoice_issued, ]; }); $all = Payable::forTenant($tenantId)->get(); $totalAmount = $all->sum('amount'); $totalPaid = $all->sum('paid_amount'); $overdueAmount = $all->where('status', 'overdue')->sum(function ($item) { return $item->amount - $item->paid_amount; }); $stats = [ 'totalAmount' => $totalAmount, 'totalPaid' => $totalPaid, 'totalUnpaid' => $totalAmount - $totalPaid, 'overdueAmount' => $overdueAmount, 'count' => $all->count(), ]; return response()->json([ 'success' => true, 'data' => $payables, 'stats' => $stats, ]); } public function store(Request $request): JsonResponse { $request->validate([ 'vendorName' => 'required|string|max:100', 'invoiceNo' => 'nullable|string|max:50', 'amount' => 'required|integer|min:0', ]); $tenantId = session('selected_tenant_id', 1); Payable::create([ 'tenant_id' => $tenantId, 'vendor_name' => $request->input('vendorName'), 'invoice_no' => $request->input('invoiceNo'), 'issue_date' => $request->input('issueDate'), 'due_date' => $request->input('dueDate'), 'category' => $request->input('category', '사무용품'), 'amount' => $request->input('amount', 0), 'paid_amount' => 0, 'status' => 'unpaid', 'description' => $request->input('description'), 'memo' => $request->input('memo'), 'tax_invoice_issued' => $request->boolean('taxInvoiceIssued', false), ]); return response()->json([ 'success' => true, 'message' => '미지급금이 등록되었습니다.', ]); } public function update(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $payable = Payable::forTenant($tenantId)->findOrFail($id); $request->validate([ 'vendorName' => 'required|string|max:100', 'invoiceNo' => 'nullable|string|max:50', 'amount' => 'required|integer|min:0', ]); $payable->update([ 'vendor_name' => $request->input('vendorName'), 'invoice_no' => $request->input('invoiceNo'), 'issue_date' => $request->input('issueDate'), 'due_date' => $request->input('dueDate'), 'category' => $request->input('category'), 'amount' => $request->input('amount'), '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([ 'success' => true, 'message' => '미지급금이 수정되었습니다.', ]); } public function pay(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $payable = Payable::forTenant($tenantId)->findOrFail($id); $request->validate([ 'payAmount' => 'required|integer|min:1', ]); $payAmount = $request->input('payAmount'); $remaining = $payable->amount - $payable->paid_amount; if ($payAmount > $remaining) { return response()->json([ 'success' => false, 'message' => '지급액이 잔액을 초과합니다.', ], 422); } $newPaid = $payable->paid_amount + $payAmount; $newStatus = $newPaid >= $payable->amount ? 'paid' : 'partial'; $payable->update([ 'paid_amount' => $newPaid, 'status' => $newStatus, ]); return response()->json([ 'success' => true, 'message' => '지급 처리되었습니다.', ]); } public function destroy(int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $payable = Payable::forTenant($tenantId)->findOrFail($id); $payable->delete(); return response()->json([ 'success' => true, 'message' => '미지급금이 삭제되었습니다.', ]); } /** * 통합 잔액 조회: 홈택스 발생 + 일반전표 상계를 거래처별로 집계 */ public function integrated(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $startDate = $request->input('startDate', date('Y-01-01')); $endDate = $request->input('endDate', date('Y-12-31')); $account = $request->input('account', 'all'); $vendor = $request->input('vendor', ''); $accountCodes = $account === 'all' ? ['204', '205'] : [$account]; // 1. 홈택스 매입세금계산서 - 대변(미지급금 발생) $hometaxQuery = HometaxInvoiceJournal::where('tenant_id', $tenantId) ->whereIn('account_code', $accountCodes) ->where('dc_type', 'credit') ->where('credit_amount', '>', 0) ->whereBetween('write_date', [$startDate, $endDate]); if ($vendor) { $hometaxQuery->where('trading_partner_name', 'like', "%{$vendor}%"); } $hometaxGrouped = (clone $hometaxQuery) ->select( 'trading_partner_name', 'account_code', 'account_name', DB::raw('SUM(credit_amount) as total_credit'), DB::raw('COUNT(*) as cnt'), DB::raw('MIN(write_date) as first_date'), DB::raw('MAX(write_date) as last_date') ) ->groupBy('trading_partner_name', 'account_code', 'account_name') ->orderByDesc('total_credit') ->get(); $hometaxDetails = (clone $hometaxQuery) ->select('id', 'trading_partner_name', 'account_code', 'account_name', 'credit_amount', 'write_date', 'description', 'nts_confirm_num') ->orderByDesc('write_date') ->get(); // 2. 일반전표 - 계정 204/205 차변(상계) + 대변(전표를 통한 발생) $journalQuery = JournalEntryLine::where('journal_entry_lines.tenant_id', $tenantId) ->whereIn('journal_entry_lines.account_code', $accountCodes) ->join('journal_entries', function ($join) { $join->on('journal_entries.id', '=', 'journal_entry_lines.journal_entry_id') ->whereNull('journal_entries.deleted_at'); }) ->whereBetween('journal_entries.entry_date', [$startDate, $endDate]); if ($vendor) { $journalQuery->where('journal_entry_lines.trading_partner_name', 'like', "%{$vendor}%"); } $journalGrouped = (clone $journalQuery) ->select( 'journal_entry_lines.trading_partner_name', 'journal_entry_lines.account_code', 'journal_entry_lines.account_name', 'journal_entry_lines.dc_type', DB::raw('SUM(journal_entry_lines.debit_amount) as total_debit'), DB::raw('SUM(journal_entry_lines.credit_amount) as total_credit'), DB::raw('COUNT(*) as cnt') ) ->groupBy('journal_entry_lines.trading_partner_name', 'journal_entry_lines.account_code', 'journal_entry_lines.account_name', 'journal_entry_lines.dc_type') ->orderByDesc('total_debit') ->get(); $journalDetails = (clone $journalQuery) ->select( 'journal_entry_lines.id', 'journal_entry_lines.trading_partner_name', 'journal_entry_lines.account_code', 'journal_entry_lines.account_name', 'journal_entry_lines.dc_type', 'journal_entry_lines.debit_amount', 'journal_entry_lines.credit_amount', 'journal_entry_lines.description', 'journal_entries.entry_date', 'journal_entries.entry_no' ) ->orderByDesc('journal_entries.entry_date') ->get(); // 3. 거래처별 잔액 계산 $vendorMap = []; // 홈택스 대변 = 발생 foreach ($hometaxGrouped as $row) { $key = $row->trading_partner_name.'|'.$row->account_code; if (! isset($vendorMap[$key])) { $vendorMap[$key] = [ 'vendorName' => $row->trading_partner_name, 'accountCode' => $row->account_code, 'accountName' => $row->account_name, 'occurred' => 0, 'offset' => 0, 'hometaxCount' => 0, 'journalDebitCount' => 0, 'journalCreditCount' => 0, ]; } $vendorMap[$key]['occurred'] += $row->total_credit; $vendorMap[$key]['hometaxCount'] += $row->cnt; } // 일반전표 차변 = 상계, 대변 = 발생 foreach ($journalGrouped as $row) { $key = $row->trading_partner_name.'|'.$row->account_code; if (! isset($vendorMap[$key])) { $vendorMap[$key] = [ 'vendorName' => $row->trading_partner_name, 'accountCode' => $row->account_code, 'accountName' => $row->account_name, 'occurred' => 0, 'offset' => 0, 'hometaxCount' => 0, 'journalDebitCount' => 0, 'journalCreditCount' => 0, ]; } if ($row->dc_type === 'debit') { $vendorMap[$key]['offset'] += $row->total_debit; $vendorMap[$key]['journalDebitCount'] += $row->cnt; } else { $vendorMap[$key]['occurred'] += $row->total_credit; $vendorMap[$key]['journalCreditCount'] += $row->cnt; } } // 잔액 계산 및 정렬 $byVendor = collect($vendorMap)->map(function ($item) { $item['balance'] = $item['occurred'] - $item['offset']; return $item; })->sortByDesc('balance')->values()->all(); // 4. 요약 통계 $totalOccurred = array_sum(array_column($byVendor, 'occurred')); $totalOffset = array_sum(array_column($byVendor, 'offset')); return response()->json([ 'success' => true, 'data' => [ 'summary' => [ 'totalOccurred' => $totalOccurred, 'totalOffset' => $totalOffset, 'totalBalance' => $totalOccurred - $totalOffset, 'hometaxCount' => $hometaxDetails->count(), 'journalCount' => $journalDetails->count(), 'vendorCount' => count($byVendor), ], 'byVendor' => $byVendor, 'hometaxDetails' => $hometaxDetails, 'journalDetails' => $journalDetails, ], ]); } /** * 홈택스 매입세금계산서 미지급금/미지급비용 대변 내역 */ public function hometaxPayables(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $startDate = $request->input('startDate', date('Y-01-01')); $endDate = $request->input('endDate', date('Y-12-31')); $account = $request->input('account', 'all'); $accountCodes = $account === 'all' ? ['204', '205'] : [$account]; $items = HometaxInvoiceJournal::where('tenant_id', $tenantId) ->whereIn('account_code', $accountCodes) ->where('dc_type', 'credit') ->where('credit_amount', '>', 0) ->whereBetween('write_date', [$startDate, $endDate]) ->select('id', 'trading_partner_name', 'account_code', 'account_name', 'credit_amount', 'write_date', 'description', 'nts_confirm_num', 'supply_amount', 'tax_amount', 'total_amount') ->orderByDesc('write_date') ->get(); $totalCredit = $items->sum('credit_amount'); return response()->json([ 'success' => true, 'data' => $items, 'summary' => [ 'totalCredit' => $totalCredit, 'count' => $items->count(), ], ]); } /** * 일반전표 미지급금/미지급비용 내역 (차변=상계, 대변=발생) */ public function journalPayables(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $startDate = $request->input('startDate', date('Y-01-01')); $endDate = $request->input('endDate', date('Y-12-31')); $account = $request->input('account', 'all'); $dcType = $request->input('dcType', 'all'); $accountCodes = $account === 'all' ? ['204', '205'] : [$account]; $query = JournalEntryLine::where('journal_entry_lines.tenant_id', $tenantId) ->whereIn('journal_entry_lines.account_code', $accountCodes) ->join('journal_entries', function ($join) { $join->on('journal_entries.id', '=', 'journal_entry_lines.journal_entry_id') ->whereNull('journal_entries.deleted_at'); }) ->whereBetween('journal_entries.entry_date', [$startDate, $endDate]); if ($dcType !== 'all') { $query->where('journal_entry_lines.dc_type', $dcType); } $items = $query->select( 'journal_entry_lines.id', 'journal_entry_lines.trading_partner_name', 'journal_entry_lines.account_code', 'journal_entry_lines.account_name', 'journal_entry_lines.dc_type', 'journal_entry_lines.debit_amount', 'journal_entry_lines.credit_amount', 'journal_entry_lines.description', 'journal_entries.entry_date', 'journal_entries.entry_no' ) ->orderByDesc('journal_entries.entry_date') ->get(); $totalDebit = $items->sum('debit_amount'); $totalCredit = $items->sum('credit_amount'); return response()->json([ 'success' => true, 'data' => $items, 'summary' => [ 'totalDebit' => $totalDebit, 'totalCredit' => $totalCredit, 'count' => $items->count(), ], ]); } }