- DailyReportExport: 어음 현황 테이블 + 합계 + 스타일링 추가 - DailyReportService: exportData에 noteReceivables 데이터 포함 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
370 lines
14 KiB
PHP
370 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Tenants\BankAccount;
|
|
use App\Models\Tenants\Bill;
|
|
use App\Models\Tenants\Deposit;
|
|
use App\Models\Tenants\ExpectedExpense;
|
|
use App\Models\Tenants\Purchase;
|
|
use App\Models\Tenants\Sale;
|
|
use App\Models\Tenants\Withdrawal;
|
|
use Carbon\Carbon;
|
|
|
|
/**
|
|
* 일일 보고서 서비스
|
|
*/
|
|
class DailyReportService extends Service
|
|
{
|
|
/**
|
|
* 어음 및 외상매출채권 현황 조회
|
|
*/
|
|
public function noteReceivables(array $params): array
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$date = isset($params['date']) ? Carbon::parse($params['date']) : Carbon::today();
|
|
|
|
// 수취어음 중 보관중 상태인 것만 조회 (만기일 기준)
|
|
$bills = Bill::where('tenant_id', $tenantId)
|
|
->where('bill_type', 'received')
|
|
->where('status', 'stored')
|
|
->where('maturity_date', '>=', $date->copy()->startOfDay())
|
|
->orderBy('maturity_date', 'asc')
|
|
->get();
|
|
|
|
return $bills->map(function ($bill) {
|
|
return [
|
|
'id' => (string) $bill->id,
|
|
'content' => "(수취어음) {$bill->display_client_name} - {$bill->bill_number}",
|
|
'current_balance' => (float) $bill->amount,
|
|
'issue_date' => $bill->issue_date?->format('Y-m-d'),
|
|
'due_date' => $bill->maturity_date?->format('Y-m-d'),
|
|
];
|
|
})->values()->toArray();
|
|
}
|
|
|
|
/**
|
|
* 일별 계좌 현황 조회
|
|
*/
|
|
public function dailyAccounts(array $params): array
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$date = isset($params['date']) ? Carbon::parse($params['date']) : Carbon::today();
|
|
$startOfMonth = $date->copy()->startOfMonth();
|
|
$endOfDay = $date->copy()->endOfDay();
|
|
|
|
// 활성 계좌 목록
|
|
$accounts = BankAccount::where('tenant_id', $tenantId)
|
|
->where('status', 'active')
|
|
->orderBy('is_primary', 'desc')
|
|
->orderBy('bank_name', 'asc')
|
|
->get();
|
|
|
|
$result = [];
|
|
|
|
foreach ($accounts as $account) {
|
|
// 전월 이월: 이번 달 1일 이전까지의 누적 잔액
|
|
$carryoverDeposits = Deposit::where('tenant_id', $tenantId)
|
|
->where('bank_account_id', $account->id)
|
|
->where('deposit_date', '<', $startOfMonth)
|
|
->sum('amount');
|
|
|
|
$carryoverWithdrawals = Withdrawal::where('tenant_id', $tenantId)
|
|
->where('bank_account_id', $account->id)
|
|
->where('withdrawal_date', '<', $startOfMonth)
|
|
->sum('amount');
|
|
|
|
$carryover = $carryoverDeposits - $carryoverWithdrawals;
|
|
|
|
// 당일 수입 (입금)
|
|
$income = Deposit::where('tenant_id', $tenantId)
|
|
->where('bank_account_id', $account->id)
|
|
->whereBetween('deposit_date', [$startOfMonth, $endOfDay])
|
|
->sum('amount');
|
|
|
|
// 당일 지출 (출금)
|
|
$expense = Withdrawal::where('tenant_id', $tenantId)
|
|
->where('bank_account_id', $account->id)
|
|
->whereBetween('withdrawal_date', [$startOfMonth, $endOfDay])
|
|
->sum('amount');
|
|
|
|
// 잔액 = 전월이월 + 수입 - 지출
|
|
$balance = $carryover + $income - $expense;
|
|
|
|
// 매칭 상태: Deposit과 Withdrawal 금액이 일치하면 matched
|
|
$matchStatus = abs($income - $expense) < 0.01 ? 'matched' : 'unmatched';
|
|
|
|
$result[] = [
|
|
'id' => (string) $account->id,
|
|
'category' => "{$account->bank_name} {$account->getMaskedAccountNumber()}",
|
|
'match_status' => $matchStatus,
|
|
'carryover' => (float) $carryover,
|
|
'income' => (float) $income,
|
|
'expense' => (float) $expense,
|
|
'balance' => (float) $balance,
|
|
'currency' => 'KRW', // 현재는 KRW만 지원
|
|
];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 일일 보고서 요약 통계
|
|
*/
|
|
public function summary(array $params): array
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$date = isset($params['date']) ? Carbon::parse($params['date']) : Carbon::today();
|
|
|
|
// 어음 합계
|
|
$noteReceivableTotal = Bill::where('tenant_id', $tenantId)
|
|
->where('bill_type', 'received')
|
|
->where('status', 'stored')
|
|
->where('maturity_date', '>=', $date->copy()->startOfDay())
|
|
->sum('amount');
|
|
|
|
// 계좌별 현황
|
|
$dailyAccounts = $this->dailyAccounts($params);
|
|
|
|
// 통화별 합계
|
|
$krwTotal = collect($dailyAccounts)
|
|
->where('currency', 'KRW')
|
|
->reduce(function ($carry, $item) {
|
|
return [
|
|
'carryover' => $carry['carryover'] + $item['carryover'],
|
|
'income' => $carry['income'] + $item['income'],
|
|
'expense' => $carry['expense'] + $item['expense'],
|
|
'balance' => $carry['balance'] + $item['balance'],
|
|
];
|
|
}, ['carryover' => 0, 'income' => 0, 'expense' => 0, 'balance' => 0]);
|
|
|
|
$usdTotal = collect($dailyAccounts)
|
|
->where('currency', 'USD')
|
|
->reduce(function ($carry, $item) {
|
|
return [
|
|
'carryover' => $carry['carryover'] + $item['carryover'],
|
|
'income' => $carry['income'] + $item['income'],
|
|
'expense' => $carry['expense'] + $item['expense'],
|
|
'balance' => $carry['balance'] + $item['balance'],
|
|
];
|
|
}, ['carryover' => 0, 'income' => 0, 'expense' => 0, 'balance' => 0]);
|
|
|
|
// 운영자금 안정성 계산
|
|
$cashAssetTotal = (float) $krwTotal['balance'];
|
|
$monthlyOperatingExpense = $this->calculateMonthlyOperatingExpense($date);
|
|
$operatingMonths = $monthlyOperatingExpense > 0
|
|
? round($cashAssetTotal / $monthlyOperatingExpense, 1)
|
|
: null;
|
|
$operatingStability = $this->getOperatingStability($operatingMonths);
|
|
|
|
// 기획서 D1.7 자금현황 카드용 필드
|
|
$receivableBalance = $this->calculateReceivableBalance($tenantId, $date);
|
|
$payableBalance = $this->calculatePayableBalance($tenantId);
|
|
$monthlyExpenseTotal = $this->calculateMonthlyExpenseTotal($tenantId, $date);
|
|
|
|
return [
|
|
'date' => $date->format('Y-m-d'),
|
|
'day_of_week' => $date->locale('ko')->dayName,
|
|
'note_receivable_total' => (float) $noteReceivableTotal,
|
|
'foreign_currency_total' => (float) $usdTotal['balance'],
|
|
'cash_asset_total' => $cashAssetTotal,
|
|
'krw_totals' => $krwTotal,
|
|
'usd_totals' => $usdTotal,
|
|
// 운영자금 안정성 지표
|
|
'monthly_operating_expense' => $monthlyOperatingExpense,
|
|
'operating_months' => $operatingMonths,
|
|
'operating_stability' => $operatingStability,
|
|
// 자금현황 카드용
|
|
'receivable_balance' => $receivableBalance,
|
|
'payable_balance' => $payableBalance,
|
|
'monthly_expense_total' => $monthlyExpenseTotal,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 엑셀 내보내기용 데이터 조합
|
|
* DailyReportExport가 기대하는 구조로 변환
|
|
*/
|
|
public function exportData(array $params): array
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$date = isset($params['date']) ? Carbon::parse($params['date']) : Carbon::today();
|
|
$dateStr = $date->format('Y-m-d');
|
|
$startOfDay = $date->copy()->startOfDay();
|
|
$endOfDay = $date->copy()->endOfDay();
|
|
|
|
// 전일 잔액 계산 (전일까지 입금 합계 - 전일까지 출금 합계)
|
|
$prevDeposits = Deposit::where('tenant_id', $tenantId)
|
|
->where('deposit_date', '<', $startOfDay)
|
|
->sum('amount');
|
|
$prevWithdrawals = Withdrawal::where('tenant_id', $tenantId)
|
|
->where('withdrawal_date', '<', $startOfDay)
|
|
->sum('amount');
|
|
$previousBalance = (float) ($prevDeposits - $prevWithdrawals);
|
|
|
|
// 당일 입금
|
|
$dailyDeposits = Deposit::where('tenant_id', $tenantId)
|
|
->whereBetween('deposit_date', [$startOfDay, $endOfDay])
|
|
->get();
|
|
$dailyDepositTotal = (float) $dailyDeposits->sum('amount');
|
|
|
|
// 당일 출금
|
|
$dailyWithdrawals = Withdrawal::where('tenant_id', $tenantId)
|
|
->whereBetween('withdrawal_date', [$startOfDay, $endOfDay])
|
|
->get();
|
|
$dailyWithdrawalTotal = (float) $dailyWithdrawals->sum('amount');
|
|
|
|
$currentBalance = $previousBalance + $dailyDepositTotal - $dailyWithdrawalTotal;
|
|
|
|
// 상세 내역 조합
|
|
$details = [];
|
|
|
|
foreach ($dailyDeposits as $d) {
|
|
$details[] = [
|
|
'type_label' => '입금',
|
|
'client_name' => $d->client?->name ?? '-',
|
|
'account_code' => $d->account_code ?? '-',
|
|
'deposit_amount' => (float) $d->amount,
|
|
'withdrawal_amount' => 0,
|
|
'description' => $d->description ?? '',
|
|
];
|
|
}
|
|
|
|
foreach ($dailyWithdrawals as $w) {
|
|
$details[] = [
|
|
'type_label' => '출금',
|
|
'client_name' => $w->client?->name ?? '-',
|
|
'account_code' => $w->account_code ?? '-',
|
|
'deposit_amount' => 0,
|
|
'withdrawal_amount' => (float) $w->amount,
|
|
'description' => $w->description ?? '',
|
|
];
|
|
}
|
|
|
|
// 어음 및 외상매출채권 현황
|
|
$noteReceivables = $this->noteReceivables($params);
|
|
|
|
return [
|
|
'date' => $dateStr,
|
|
'previous_balance' => $previousBalance,
|
|
'daily_deposit' => $dailyDepositTotal,
|
|
'daily_withdrawal' => $dailyWithdrawalTotal,
|
|
'current_balance' => $currentBalance,
|
|
'details' => $details,
|
|
'note_receivables' => $noteReceivables,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 미수금 잔액 계산
|
|
* = 전체 매출 - 전체 입금 - 전체 수취어음 (기준일까지)
|
|
* ReceivablesService.getTotalCarryForwardBalance() 동일 로직
|
|
*/
|
|
private function calculateReceivableBalance(int $tenantId, Carbon $date): float
|
|
{
|
|
$endDate = $date->format('Y-m-d');
|
|
|
|
$totalSales = Sale::where('tenant_id', $tenantId)
|
|
->whereNotNull('client_id')
|
|
->where('sale_date', '<=', $endDate)
|
|
->sum('total_amount');
|
|
|
|
$totalDeposits = Deposit::where('tenant_id', $tenantId)
|
|
->whereNotNull('client_id')
|
|
->where('deposit_date', '<=', $endDate)
|
|
->sum('amount');
|
|
|
|
$totalBills = Bill::where('tenant_id', $tenantId)
|
|
->whereNotNull('client_id')
|
|
->where('bill_type', 'received')
|
|
->where('issue_date', '<=', $endDate)
|
|
->sum('amount');
|
|
|
|
return (float) ($totalSales - $totalDeposits - $totalBills);
|
|
}
|
|
|
|
/**
|
|
* 미지급금 잔액 계산
|
|
* = 미지급 상태(pending, partial, overdue)인 ExpectedExpense 합계
|
|
*/
|
|
private function calculatePayableBalance(int $tenantId): float
|
|
{
|
|
return (float) ExpectedExpense::where('tenant_id', $tenantId)
|
|
->whereIn('payment_status', ['pending', 'partial', 'overdue'])
|
|
->sum('amount');
|
|
}
|
|
|
|
/**
|
|
* 당월 예상 지출 합계 계산
|
|
* = 당월 매입(Purchase) + 당월 예상지출(ExpectedExpense)
|
|
*/
|
|
private function calculateMonthlyExpenseTotal(int $tenantId, Carbon $date): float
|
|
{
|
|
$startOfMonth = $date->copy()->startOfMonth()->format('Y-m-d');
|
|
$endOfMonth = $date->copy()->endOfMonth()->format('Y-m-d');
|
|
|
|
// 당월 매입 합계
|
|
$purchaseTotal = Purchase::where('tenant_id', $tenantId)
|
|
->whereBetween('purchase_date', [$startOfMonth, $endOfMonth])
|
|
->sum('total_amount');
|
|
|
|
// 당월 예상 지출 합계 (매입 외: 카드, 어음, 급여, 임대료 등)
|
|
$expectedExpenseTotal = ExpectedExpense::where('tenant_id', $tenantId)
|
|
->whereBetween('expected_payment_date', [$startOfMonth, $endOfMonth])
|
|
->sum('amount');
|
|
|
|
return (float) ($purchaseTotal + $expectedExpenseTotal);
|
|
}
|
|
|
|
/**
|
|
* 직전 3개월 평균 월 운영비 계산
|
|
*
|
|
* @param Carbon $baseDate 기준일
|
|
* @return float 월 평균 운영비
|
|
*/
|
|
private function calculateMonthlyOperatingExpense(Carbon $baseDate): float
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
// 직전 3개월 범위: 기준일 전월 말일부터 3개월 전 1일까지
|
|
$lastMonthEnd = $baseDate->copy()->subMonth()->endOfMonth();
|
|
$threeMonthsAgo = $baseDate->copy()->subMonths(3)->startOfMonth();
|
|
|
|
$totalExpense = Withdrawal::where('tenant_id', $tenantId)
|
|
->whereBetween('withdrawal_date', [$threeMonthsAgo, $lastMonthEnd])
|
|
->sum('amount');
|
|
|
|
// 3개월 평균
|
|
return round((float) $totalExpense / 3, 0);
|
|
}
|
|
|
|
/**
|
|
* 운영자금 안정성 판정
|
|
*
|
|
* 색상 가이드 기준:
|
|
* - warning (빨강): 3개월 미만 - 자금 부족 우려
|
|
* - caution (주황): 3~6개월 - 자금 관리 필요
|
|
* - stable (파랑): 6개월 이상 - 안정적
|
|
*
|
|
* @param float|null $months 운영 가능 개월 수
|
|
* @return string 안정성 상태 (stable|caution|warning|unknown)
|
|
*/
|
|
private function getOperatingStability(?float $months): string
|
|
{
|
|
if ($months === null) {
|
|
return 'unknown';
|
|
}
|
|
|
|
if ($months >= 6) {
|
|
return 'stable'; // 파랑 - 안정적
|
|
}
|
|
|
|
if ($months >= 3) {
|
|
return 'caution'; // 주황 - 자금 관리 필요
|
|
}
|
|
|
|
return 'warning'; // 빨강 - 자금 부족 우려
|
|
}
|
|
}
|