feat: [receivables] 미수금 이월잔액 반영

- summary() 메서드: 거래처별 이월잔액(priorBalance) 계산 추가
- ledger() 메서드: 원장 누적잔액이 이월잔액부터 시작하도록 변경
- 프론트엔드: LedgerTab 이월잔액 통계카드 추가 (조건부 표시)
- 프론트엔드: SummaryTab 이월잔액 컬럼 추가, 라벨 당기발생/당기회수로 변경
- CSV 다운로드에 이월잔액 포함
This commit is contained in:
김보곤
2026-02-25 09:35:51 +09:00
parent daf4b20fe8
commit 9c21ff9720
2 changed files with 183 additions and 30 deletions

View File

@@ -9,6 +9,7 @@
use App\Models\Finance\Receivable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ReceivableController extends Controller
{
@@ -194,6 +195,53 @@ public function ledger(Request $request): JsonResponse
$partner = $request->input('partner');
$source = $request->input('source', 'all');
// 분개 완료된 매출세금계산서 ID
$journaledInvoiceIds = [];
if ($source !== 'journal') {
$journaledInvoiceIds = HometaxInvoiceJournal::where('tenant_id', $tenantId)
->where('invoice_type', 'sales')
->distinct()
->pluck('hometax_invoice_id')
->toArray();
}
// 이월 잔액 계산 (startDate 이전)
$priorBalance = 0;
if ($startDate) {
if ($source !== 'journal') {
$priorHtQuery = HometaxInvoiceJournal::where('tenant_id', $tenantId)
->where('account_code', '108')
->where('invoice_type', 'sales')
->where('write_date', '<', $startDate);
if ($partner) {
$priorHtQuery->where('trading_partner_name', 'like', "%{$partner}%");
}
$priorHtData = $priorHtQuery->selectRaw('SUM(debit_amount) as td, SUM(credit_amount) as tc')->first();
$priorBalance += ($priorHtData->td ?? 0) - ($priorHtData->tc ?? 0);
$priorUnjQuery = HometaxInvoice::where('tenant_id', $tenantId)
->where('invoice_type', 'sales')
->whereNotIn('id', $journaledInvoiceIds)
->where('write_date', '<', $startDate);
if ($partner) {
$priorUnjQuery->where('invoicee_corp_name', 'like', "%{$partner}%");
}
$priorBalance += (int) $priorUnjQuery->sum('total_amount');
}
if ($source !== 'hometax') {
$priorJnlQuery = 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')
->where('journal_entries.entry_date', '<', $startDate);
if ($partner) {
$priorJnlQuery->where('journal_entry_lines.trading_partner_name', 'like', "%{$partner}%");
}
$priorJnlData = $priorJnlQuery->selectRaw('SUM(journal_entry_lines.debit_amount) as td, SUM(journal_entry_lines.credit_amount) as tc')->first();
$priorBalance += ($priorJnlData->td ?? 0) - ($priorJnlData->tc ?? 0);
}
}
$items = collect();
// 1) 홈택스 매출세금계산서에서 외상매출금 조회
@@ -213,12 +261,6 @@ public function ledger(Request $request): JsonResponse
$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',
@@ -293,9 +335,9 @@ public function ledger(Request $request): JsonResponse
$items = $items->merge($journalItems);
}
// 날짜순 정렬 + 누적잔액 계산
// 날짜순 정렬 + 누적잔액 계산 (이월잔액부터 시작)
$sorted = $items->sortBy('date')->values();
$balance = 0;
$balance = $priorBalance;
$sorted = $sorted->map(function ($item) use (&$balance) {
$balance += $item['debitAmount'] - $item['creditAmount'];
$item['balance'] = $balance;
@@ -311,16 +353,17 @@ public function ledger(Request $request): JsonResponse
'success' => true,
'data' => $sorted->values(),
'stats' => [
'priorBalance' => $priorBalance,
'totalDebit' => $totalDebit,
'totalCredit' => $totalCredit,
'balance' => $totalDebit - $totalCredit,
'balance' => $priorBalance + $totalDebit - $totalCredit,
'partnerCount' => $partnerCount,
],
]);
}
/**
* 거래처별 외상매출금 요약
* 거래처별 외상매출금 요약 (이월잔액 포함)
*/
public function summary(Request $request): JsonResponse
{
@@ -328,6 +371,79 @@ public function summary(Request $request): JsonResponse
$startDate = $request->input('startDate');
$endDate = $request->input('endDate');
// 분개 완료된 매출세금계산서 ID (전 기간)
$journaledInvoiceIds = HometaxInvoiceJournal::where('tenant_id', $tenantId)
->where('invoice_type', 'sales')
->distinct()
->pluck('hometax_invoice_id')
->toArray();
// 0. 이월 잔액 계산 (startDate 이전의 누적 잔액)
$priorBalanceMap = [];
if ($startDate) {
// 이월 - 홈택스 분개 완료건 (account 108, sales)
HometaxInvoiceJournal::where('tenant_id', $tenantId)
->where('account_code', '108')
->where('invoice_type', 'sales')
->where('write_date', '<', $startDate)
->select(
'trading_partner_name',
DB::raw('SUM(debit_amount) as total_debit'),
DB::raw('SUM(credit_amount) as total_credit')
)
->groupBy('trading_partner_name')
->get()
->each(function ($row) use (&$priorBalanceMap) {
$key = $row->trading_partner_name;
if (! isset($priorBalanceMap[$key])) {
$priorBalanceMap[$key] = ['debit' => 0, 'credit' => 0];
}
$priorBalanceMap[$key]['debit'] += $row->total_debit;
$priorBalanceMap[$key]['credit'] += $row->total_credit;
});
// 이월 - 미분개 매출세금계산서 (차변 발생)
HometaxInvoice::where('tenant_id', $tenantId)
->where('invoice_type', 'sales')
->whereNotIn('id', $journaledInvoiceIds)
->where('write_date', '<', $startDate)
->select('invoicee_corp_name', DB::raw('SUM(total_amount) as total_amount'))
->groupBy('invoicee_corp_name')
->get()
->each(function ($row) use (&$priorBalanceMap) {
$key = $row->invoicee_corp_name;
if (! isset($priorBalanceMap[$key])) {
$priorBalanceMap[$key] = ['debit' => 0, 'credit' => 0];
}
$priorBalanceMap[$key]['debit'] += $row->total_amount;
});
// 이월 - 일반전표 (account 108)
JournalEntryLine::where('journal_entry_lines.tenant_id', $tenantId)
->where('journal_entry_lines.account_code', '108')
->join('journal_entries', function ($join) {
$join->on('journal_entries.id', '=', 'journal_entry_lines.journal_entry_id')
->whereNull('journal_entries.deleted_at');
})
->where('journal_entries.entry_date', '<', $startDate)
->select(
'journal_entry_lines.trading_partner_name',
DB::raw('SUM(journal_entry_lines.debit_amount) as total_debit'),
DB::raw('SUM(journal_entry_lines.credit_amount) as total_credit')
)
->groupBy('journal_entry_lines.trading_partner_name')
->get()
->each(function ($row) use (&$priorBalanceMap) {
$key = $row->trading_partner_name;
if (! isset($priorBalanceMap[$key])) {
$priorBalanceMap[$key] = ['debit' => 0, 'credit' => 0];
}
$priorBalanceMap[$key]['debit'] += $row->total_debit;
$priorBalanceMap[$key]['credit'] += $row->total_credit;
});
}
$items = collect();
// 1) 홈택스 매출세금계산서에서 외상매출금
@@ -343,12 +459,6 @@ public function summary(Request $request): JsonResponse
$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,
@@ -399,20 +509,42 @@ public function summary(Request $request): JsonResponse
]);
$items = $items->merge($journalItems);
// 거래처별 그룹핑
$grouped = $items->groupBy('tradingPartnerName')->map(function ($group, $partnerName) {
// 거래처별 그룹핑 (이월 잔액 포함)
$grouped = $items->groupBy('tradingPartnerName')->map(function ($group, $partnerName) use ($priorBalanceMap) {
$totalDebit = $group->sum('debitAmount');
$totalCredit = $group->sum('creditAmount');
$prior = $priorBalanceMap[$partnerName] ?? null;
$priorBalance = $prior ? ($prior['debit'] - $prior['credit']) : 0;
return [
'tradingPartnerName' => $partnerName ?: '(미지정)',
'priorBalance' => $priorBalance,
'totalDebit' => $totalDebit,
'totalCredit' => $totalCredit,
'balance' => $totalDebit - $totalCredit,
'balance' => $priorBalance + $totalDebit - $totalCredit,
'lastTransactionDate' => $group->pluck('date')->filter()->sort()->last(),
'transactionCount' => $group->count(),
];
})->sortByDesc('balance')->values();
});
// 이월 잔액만 있고 당기 거래가 없는 거래처도 포함
$existingPartners = $grouped->keys()->toArray();
foreach ($priorBalanceMap as $partnerName => $prior) {
$priorBalance = $prior['debit'] - $prior['credit'];
if ($priorBalance != 0 && ! in_array($partnerName, $existingPartners)) {
$grouped[$partnerName] = [
'tradingPartnerName' => $partnerName ?: '(미지정)',
'priorBalance' => $priorBalance,
'totalDebit' => 0,
'totalCredit' => 0,
'balance' => $priorBalance,
'lastTransactionDate' => null,
'transactionCount' => 0,
];
}
}
$grouped = $grouped->sortByDesc('balance')->values();
return response()->json([
'success' => true,