input('search')) { $query->where(function ($q) use ($search) { $q->where('customer_name', 'like', "%{$search}%") ->orWhere('invoice_no', '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); } } $receivables = $query->orderBy('created_at', 'desc') ->get() ->map(function ($item) { return [ 'id' => $item->id, 'customerName' => $item->customer_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, 'collectedAmount' => $item->collected_amount, 'status' => $item->status, 'description' => $item->description, 'memo' => $item->memo, ]; }); $allQuery = Receivable::forTenant($tenantId); $all = (clone $allQuery)->get(); $totalAmount = $all->sum('amount'); $totalCollected = $all->sum('collected_amount'); $overdueAmount = $all->where('status', 'overdue')->sum(function ($item) { return $item->amount - $item->collected_amount; }); $stats = [ 'totalAmount' => $totalAmount, 'totalCollected' => $totalCollected, 'totalOutstanding' => $totalAmount - $totalCollected, 'overdueAmount' => $overdueAmount, 'count' => $all->count(), ]; return response()->json([ 'success' => true, 'data' => $receivables, 'stats' => $stats, ]); } public function store(Request $request): JsonResponse { $request->validate([ 'customerName' => 'required|string|max:100', 'invoiceNo' => 'required|string|max:50', 'amount' => 'required|integer|min:0', ]); $tenantId = session('selected_tenant_id', 1); $receivable = Receivable::create([ 'tenant_id' => $tenantId, 'customer_name' => $request->input('customerName'), 'invoice_no' => $request->input('invoiceNo'), 'issue_date' => $request->input('issueDate'), 'due_date' => $request->input('dueDate'), 'category' => $request->input('category', '서비스'), 'amount' => $request->input('amount', 0), 'collected_amount' => 0, 'status' => 'outstanding', 'description' => $request->input('description'), 'memo' => $request->input('memo'), ]); return response()->json([ 'success' => true, 'message' => '미수금이 등록되었습니다.', ]); } public function update(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $receivable = Receivable::forTenant($tenantId)->findOrFail($id); $request->validate([ 'customerName' => 'required|string|max:100', 'invoiceNo' => 'required|string|max:50', 'amount' => 'required|integer|min:0', ]); $receivable->update([ 'customer_name' => $request->input('customerName'), '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', $receivable->status), 'description' => $request->input('description'), 'memo' => $request->input('memo'), ]); return response()->json([ 'success' => true, 'message' => '미수금이 수정되었습니다.', ]); } public function collect(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $receivable = Receivable::forTenant($tenantId)->findOrFail($id); $request->validate([ 'collectAmount' => 'required|integer|min:1', ]); $collectAmount = $request->input('collectAmount'); $remaining = $receivable->amount - $receivable->collected_amount; if ($collectAmount > $remaining) { return response()->json([ 'success' => false, 'message' => '수금액이 잔액을 초과합니다.', ], 422); } $newCollected = $receivable->collected_amount + $collectAmount; $newStatus = $newCollected >= $receivable->amount ? 'collected' : 'partial'; $receivable->update([ 'collected_amount' => $newCollected, 'status' => $newStatus, ]); return response()->json([ 'success' => true, 'message' => '수금 처리되었습니다.', ]); } public function destroy(int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $receivable = Receivable::forTenant($tenantId)->findOrFail($id); $receivable->delete(); return response()->json([ 'success' => true, 'message' => '미수금이 삭제되었습니다.', ]); } /** * 외상매출금 원장 (홈택스 분개 + 일반전표에서 계정코드 108 집계) */ public function ledger(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $startDate = $request->input('startDate'); $endDate = $request->input('endDate'); $partner = $request->input('partner'); $source = $request->input('source', 'all'); $items = collect(); // 1) 홈택스 매출세금계산서에서 외상매출금 조회 if ($source !== 'journal') { // 1-a) 분개 완료된 건: hometax_invoice_journals에서 계정코드 108 $hometaxQuery = HometaxInvoiceJournal::where('tenant_id', $tenantId) ->where('account_code', '108') ->where('invoice_type', 'sales'); if ($startDate) { $hometaxQuery->where('write_date', '>=', $startDate); } if ($endDate) { $hometaxQuery->where('write_date', '<=', $endDate); } if ($partner) { $hometaxQuery->where('trading_partner_name', 'like', "%{$partner}%"); } $journaledInvoiceIds = HometaxInvoiceJournal::where('tenant_id', $tenantId) ->where('invoice_type', 'sales') ->distinct() ->pluck('hometax_invoice_id') ->toArray(); $hometaxItems = $hometaxQuery->get()->map(fn ($j) => [ 'date' => $j->write_date?->format('Y-m-d'), 'source' => 'hometax', 'sourceLabel' => '홈택스 매출', 'refNo' => $j->nts_confirm_num, 'tradingPartnerName' => $j->trading_partner_name, 'description' => $j->description ?: '매출세금계산서', 'debitAmount' => $j->dc_type === 'debit' ? (int) $j->debit_amount : 0, 'creditAmount' => $j->dc_type === 'credit' ? (int) $j->credit_amount : 0, ]); $items = $items->merge($hometaxItems); // 1-b) 미분개 매출세금계산서: total_amount를 차변(발생)으로 표시 $unjournaledQuery = HometaxInvoice::where('tenant_id', $tenantId) ->where('invoice_type', 'sales') ->whereNotIn('id', $journaledInvoiceIds); if ($startDate) { $unjournaledQuery->where('write_date', '>=', $startDate); } if ($endDate) { $unjournaledQuery->where('write_date', '<=', $endDate); } if ($partner) { $unjournaledQuery->where('invoicee_corp_name', 'like', "%{$partner}%"); } $unjournaledItems = $unjournaledQuery->get()->map(fn ($inv) => [ 'date' => $inv->write_date?->format('Y-m-d'), 'source' => 'hometax', 'sourceLabel' => '홈택스 매출 (미분개)', 'refNo' => $inv->nts_confirm_num, 'tradingPartnerName' => $inv->invoicee_corp_name, 'description' => '매출세금계산서 (미분개)', 'debitAmount' => (int) $inv->total_amount, 'creditAmount' => 0, ]); $items = $items->merge($unjournaledItems); } // 2) 일반전표에서 외상매출금(108) 조회 if ($source !== 'hometax') { $journalQuery = JournalEntryLine::where('journal_entry_lines.tenant_id', $tenantId) ->where('journal_entry_lines.account_code', '108') ->join('journal_entries', 'journal_entries.id', '=', 'journal_entry_lines.journal_entry_id') ->whereNull('journal_entries.deleted_at') ->select('journal_entry_lines.*', 'journal_entries.entry_date', 'journal_entries.entry_no'); if ($startDate) { $journalQuery->where('journal_entries.entry_date', '>=', $startDate); } if ($endDate) { $journalQuery->where('journal_entries.entry_date', '<=', $endDate); } if ($partner) { $journalQuery->where('journal_entry_lines.trading_partner_name', 'like', "%{$partner}%"); } $journalItems = $journalQuery->get()->map(fn ($l) => [ 'date' => $l->entry_date instanceof \Carbon\Carbon ? $l->entry_date->format('Y-m-d') : $l->entry_date, 'source' => 'journal', 'sourceLabel' => '일반전표 '.$l->entry_no, 'refNo' => $l->entry_no, 'tradingPartnerName' => $l->trading_partner_name, 'description' => $l->description ?: '일반전표', 'debitAmount' => (int) $l->debit_amount, 'creditAmount' => (int) $l->credit_amount, ]); $items = $items->merge($journalItems); } // 날짜순 정렬 + 누적잔액 계산 $sorted = $items->sortBy('date')->values(); $balance = 0; $sorted = $sorted->map(function ($item) use (&$balance) { $balance += $item['debitAmount'] - $item['creditAmount']; $item['balance'] = $balance; return $item; }); $totalDebit = $sorted->sum('debitAmount'); $totalCredit = $sorted->sum('creditAmount'); $partnerCount = $sorted->pluck('tradingPartnerName')->filter()->unique()->count(); return response()->json([ 'success' => true, 'data' => $sorted->values(), 'stats' => [ 'totalDebit' => $totalDebit, 'totalCredit' => $totalCredit, 'balance' => $totalDebit - $totalCredit, 'partnerCount' => $partnerCount, ], ]); } /** * 거래처별 외상매출금 요약 */ public function summary(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $startDate = $request->input('startDate'); $endDate = $request->input('endDate'); $items = collect(); // 1) 홈택스 매출세금계산서에서 외상매출금 // 1-a) 분개 완료된 건 $hometaxQuery = HometaxInvoiceJournal::where('tenant_id', $tenantId) ->where('account_code', '108') ->where('invoice_type', 'sales'); if ($startDate) { $hometaxQuery->where('write_date', '>=', $startDate); } if ($endDate) { $hometaxQuery->where('write_date', '<=', $endDate); } $journaledInvoiceIds = HometaxInvoiceJournal::where('tenant_id', $tenantId) ->where('invoice_type', 'sales') ->distinct() ->pluck('hometax_invoice_id') ->toArray(); $hometaxItems = $hometaxQuery->get()->map(fn ($j) => [ 'tradingPartnerName' => $j->trading_partner_name, 'debitAmount' => $j->dc_type === 'debit' ? (int) $j->debit_amount : 0, 'creditAmount' => $j->dc_type === 'credit' ? (int) $j->credit_amount : 0, 'date' => $j->write_date?->format('Y-m-d'), ]); $items = $items->merge($hometaxItems); // 1-b) 미분개 매출세금계산서 $unjournaledQuery = HometaxInvoice::where('tenant_id', $tenantId) ->where('invoice_type', 'sales') ->whereNotIn('id', $journaledInvoiceIds); if ($startDate) { $unjournaledQuery->where('write_date', '>=', $startDate); } if ($endDate) { $unjournaledQuery->where('write_date', '<=', $endDate); } $unjournaledItems = $unjournaledQuery->get()->map(fn ($inv) => [ 'tradingPartnerName' => $inv->invoicee_corp_name, 'debitAmount' => (int) $inv->total_amount, 'creditAmount' => 0, 'date' => $inv->write_date?->format('Y-m-d'), ]); $items = $items->merge($unjournaledItems); // 2) 일반전표에서 외상매출금(108) $journalQuery = JournalEntryLine::where('journal_entry_lines.tenant_id', $tenantId) ->where('journal_entry_lines.account_code', '108') ->join('journal_entries', 'journal_entries.id', '=', 'journal_entry_lines.journal_entry_id') ->whereNull('journal_entries.deleted_at') ->select('journal_entry_lines.*', 'journal_entries.entry_date'); if ($startDate) { $journalQuery->where('journal_entries.entry_date', '>=', $startDate); } if ($endDate) { $journalQuery->where('journal_entries.entry_date', '<=', $endDate); } $journalItems = $journalQuery->get()->map(fn ($l) => [ 'tradingPartnerName' => $l->trading_partner_name, 'debitAmount' => (int) $l->debit_amount, 'creditAmount' => (int) $l->credit_amount, 'date' => $l->entry_date instanceof \Carbon\Carbon ? $l->entry_date->format('Y-m-d') : $l->entry_date, ]); $items = $items->merge($journalItems); // 거래처별 그룹핑 $grouped = $items->groupBy('tradingPartnerName')->map(function ($group, $partnerName) { $totalDebit = $group->sum('debitAmount'); $totalCredit = $group->sum('creditAmount'); return [ 'tradingPartnerName' => $partnerName ?: '(미지정)', 'totalDebit' => $totalDebit, 'totalCredit' => $totalCredit, 'balance' => $totalDebit - $totalCredit, 'lastTransactionDate' => $group->pluck('date')->filter()->sort()->last(), 'transactionCount' => $group->count(), ]; })->sortByDesc('balance')->values(); return response()->json([ 'success' => true, 'data' => $grouped, ]); } }