2025-12-26 15:47:14 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\Orders\Client;
|
|
|
|
|
use App\Models\Tenants\Bill;
|
|
|
|
|
use App\Models\Tenants\Deposit;
|
|
|
|
|
use App\Models\Tenants\Sale;
|
2026-01-02 14:47:51 +09:00
|
|
|
use Carbon\Carbon;
|
2025-12-26 15:47:14 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 채권 현황 서비스
|
|
|
|
|
* 거래처별 월별 매출, 입금, 어음, 미수금 현황 조회
|
2026-01-02 14:47:51 +09:00
|
|
|
* - 동적 월 표시 지원 (최근 1년: 동적 12개월)
|
|
|
|
|
* - 이월잔액 + 누적 미수금 계산
|
2025-12-26 15:47:14 +09:00
|
|
|
*/
|
|
|
|
|
class ReceivablesService extends Service
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 채권 현황 목록 조회
|
|
|
|
|
*/
|
|
|
|
|
public function index(array $params): array
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
2026-01-02 14:47:51 +09:00
|
|
|
$recentYear = $params['recent_year'] ?? false;
|
2025-12-26 15:47:14 +09:00
|
|
|
$year = $params['year'] ?? date('Y');
|
|
|
|
|
$search = $params['search'] ?? null;
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 월 기간 생성 (동적 월 지원)
|
|
|
|
|
$periods = $this->generateMonthPeriods($recentYear, $year);
|
|
|
|
|
$monthLabels = array_map(fn ($p) => $p['label'], $periods);
|
|
|
|
|
|
|
|
|
|
// 이월잔액 기준일 (첫번째 월의 시작일 전날)
|
|
|
|
|
$carryForwardDate = Carbon::parse($periods[0]['start'])->subDay()->format('Y-m-d');
|
|
|
|
|
|
2025-12-26 15:47:14 +09:00
|
|
|
// 거래처 목록 조회
|
|
|
|
|
$clientsQuery = Client::where('tenant_id', $tenantId)
|
|
|
|
|
->where('is_active', true);
|
|
|
|
|
|
|
|
|
|
if ($search) {
|
|
|
|
|
$clientsQuery->where('name', 'like', "%{$search}%");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$clients = $clientsQuery->orderBy('name')->get();
|
|
|
|
|
|
|
|
|
|
$result = [];
|
|
|
|
|
|
|
|
|
|
foreach ($clients as $client) {
|
2026-01-02 14:47:51 +09:00
|
|
|
// 이월잔액 계산 (기준일 이전까지의 누적 미수금)
|
|
|
|
|
$carryForwardBalance = $this->getCarryForwardBalance($tenantId, $client->id, $carryForwardDate);
|
|
|
|
|
|
|
|
|
|
// 월별 데이터 수집 (년-월 키 기반)
|
|
|
|
|
$salesByPeriod = $this->getSalesByPeriods($tenantId, $client->id, $periods);
|
|
|
|
|
$depositsByPeriod = $this->getDepositsByPeriods($tenantId, $client->id, $periods);
|
|
|
|
|
$billsByPeriod = $this->getBillsByPeriods($tenantId, $client->id, $periods);
|
|
|
|
|
|
|
|
|
|
// 누적 미수금 계산
|
|
|
|
|
$receivablesByPeriod = $this->calculateCumulativeReceivables(
|
|
|
|
|
$carryForwardBalance,
|
|
|
|
|
$salesByPeriod,
|
|
|
|
|
$depositsByPeriod,
|
|
|
|
|
$billsByPeriod,
|
|
|
|
|
count($periods)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 카테고리별 데이터 생성 (배열 형태)
|
2025-12-26 15:47:14 +09:00
|
|
|
$categories = [
|
|
|
|
|
[
|
|
|
|
|
'category' => 'sales',
|
2026-01-02 14:47:51 +09:00
|
|
|
'amounts' => $this->formatPeriodAmounts($salesByPeriod, count($periods)),
|
2025-12-26 15:47:14 +09:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'category' => 'deposit',
|
2026-01-02 14:47:51 +09:00
|
|
|
'amounts' => $this->formatPeriodAmounts($depositsByPeriod, count($periods)),
|
2025-12-26 15:47:14 +09:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'category' => 'bill',
|
2026-01-02 14:47:51 +09:00
|
|
|
'amounts' => $this->formatPeriodAmounts($billsByPeriod, count($periods)),
|
2025-12-26 15:47:14 +09:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'category' => 'receivable',
|
2026-01-02 14:47:51 +09:00
|
|
|
'amounts' => $this->formatReceivableAmounts($receivablesByPeriod),
|
2025-12-26 15:47:14 +09:00
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 연체 여부: 최종 미수금이 양수인 경우
|
|
|
|
|
$finalReceivable = end($receivablesByPeriod);
|
|
|
|
|
$isOverdue = $client->is_overdue ?? ($finalReceivable > 0);
|
2025-12-26 15:47:14 +09:00
|
|
|
|
|
|
|
|
$result[] = [
|
|
|
|
|
'id' => (string) $client->id,
|
|
|
|
|
'vendor_id' => $client->id,
|
|
|
|
|
'vendor_name' => $client->name,
|
2026-01-02 14:47:51 +09:00
|
|
|
'is_overdue' => $isOverdue,
|
|
|
|
|
'memo' => $client->memo ?? '',
|
|
|
|
|
'carry_forward_balance' => $carryForwardBalance,
|
|
|
|
|
'month_labels' => $monthLabels,
|
2025-12-26 15:47:14 +09:00
|
|
|
'categories' => $categories,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 미수금이 있는 거래처만 필터링 (선택적)
|
|
|
|
|
if (! empty($params['has_receivable'])) {
|
|
|
|
|
$result = array_filter($result, function ($item) {
|
|
|
|
|
$receivableCat = collect($item['categories'])->firstWhere('category', 'receivable');
|
|
|
|
|
|
|
|
|
|
return $receivableCat && $receivableCat['amounts']['total'] > 0;
|
|
|
|
|
});
|
|
|
|
|
$result = array_values($result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 공통 월 레이블 추가 (프론트엔드에서 헤더로 사용)
|
|
|
|
|
return [
|
|
|
|
|
'month_labels' => $monthLabels,
|
|
|
|
|
'items' => $result,
|
|
|
|
|
];
|
2025-12-26 15:47:14 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-04 14:21:05 +09:00
|
|
|
* 요약 통계 조회 (D1.7 cards + check_points 구조)
|
|
|
|
|
*
|
|
|
|
|
* @return array{cards: array, check_points: array}
|
2025-12-26 15:47:14 +09:00
|
|
|
*/
|
|
|
|
|
public function summary(array $params): array
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
2026-03-04 14:21:05 +09:00
|
|
|
$now = Carbon::now();
|
2026-01-02 14:47:51 +09:00
|
|
|
$recentYear = $params['recent_year'] ?? false;
|
2025-12-26 15:47:14 +09:00
|
|
|
$year = $params['year'] ?? date('Y');
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 월 기간 생성
|
|
|
|
|
$periods = $this->generateMonthPeriods($recentYear, $year);
|
|
|
|
|
$startDate = $periods[0]['start'];
|
|
|
|
|
$endDate = end($periods)['end'];
|
2025-12-26 15:47:14 +09:00
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 이월잔액 기준일
|
|
|
|
|
$carryForwardDate = Carbon::parse($startDate)->subDay()->format('Y-m-d');
|
|
|
|
|
|
|
|
|
|
// 전체 이월잔액 (모든 거래처)
|
|
|
|
|
$totalCarryForward = $this->getTotalCarryForwardBalance($tenantId, $carryForwardDate);
|
|
|
|
|
|
|
|
|
|
// 기간 내 총 매출
|
2026-03-04 14:21:05 +09:00
|
|
|
$totalSales = (float) Sale::where('tenant_id', $tenantId)
|
2025-12-26 15:47:14 +09:00
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->whereBetween('sale_date', [$startDate, $endDate])
|
|
|
|
|
->sum('total_amount');
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 기간 내 총 입금
|
2026-03-04 14:21:05 +09:00
|
|
|
$totalDeposits = (float) Deposit::where('tenant_id', $tenantId)
|
2025-12-26 15:47:14 +09:00
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->whereBetween('deposit_date', [$startDate, $endDate])
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 기간 내 총 어음
|
2026-03-04 14:21:05 +09:00
|
|
|
$totalBills = (float) Bill::where('tenant_id', $tenantId)
|
2025-12-26 15:47:14 +09:00
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->where('bill_type', 'received')
|
|
|
|
|
->whereBetween('issue_date', [$startDate, $endDate])
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 총 미수금 (이월잔액 + 매출 - 입금 - 어음)
|
|
|
|
|
$totalReceivables = $totalCarryForward + $totalSales - $totalDeposits - $totalBills;
|
2025-12-26 15:47:14 +09:00
|
|
|
|
2026-03-04 14:21:05 +09:00
|
|
|
// 당월 미수금
|
|
|
|
|
$currentMonthStart = $now->copy()->startOfMonth()->format('Y-m-d');
|
|
|
|
|
$currentMonthEnd = $now->copy()->endOfMonth()->format('Y-m-d');
|
|
|
|
|
|
|
|
|
|
$currentMonthSales = (float) Sale::where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->whereBetween('sale_date', [$currentMonthStart, $currentMonthEnd])
|
|
|
|
|
->sum('total_amount');
|
|
|
|
|
|
|
|
|
|
$currentMonthDeposits = (float) Deposit::where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->whereBetween('deposit_date', [$currentMonthStart, $currentMonthEnd])
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
$currentMonthReceivables = $currentMonthSales - $currentMonthDeposits;
|
|
|
|
|
|
2025-12-26 15:47:14 +09:00
|
|
|
// 거래처 수
|
|
|
|
|
$vendorCount = Client::where('tenant_id', $tenantId)
|
|
|
|
|
->where('is_active', true)
|
|
|
|
|
->count();
|
|
|
|
|
|
2026-03-04 14:21:05 +09:00
|
|
|
// 연체 거래처 수
|
2026-01-02 14:47:51 +09:00
|
|
|
$overdueVendorCount = Client::where('tenant_id', $tenantId)
|
|
|
|
|
->where('is_active', true)
|
|
|
|
|
->where('is_overdue', true)
|
2025-12-26 15:47:14 +09:00
|
|
|
->count();
|
|
|
|
|
|
2026-03-04 14:21:05 +09:00
|
|
|
// 악성채권 건수
|
|
|
|
|
$badDebtCount = $this->getBadDebtCount($tenantId);
|
|
|
|
|
|
|
|
|
|
// Top 3 미수금 거래처
|
|
|
|
|
$topVendors = $this->getTopReceivableVendors($tenantId, 3);
|
|
|
|
|
|
|
|
|
|
// 카드 데이터 구성
|
|
|
|
|
$cards = [
|
|
|
|
|
[
|
|
|
|
|
'id' => 'rv_cumulative',
|
|
|
|
|
'label' => '누적 미수금',
|
|
|
|
|
'amount' => (int) $totalReceivables,
|
|
|
|
|
'sub_items' => [
|
|
|
|
|
['label' => '매출', 'value' => (int) $totalSales],
|
|
|
|
|
['label' => '입금', 'value' => (int) $totalDeposits],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 'rv_monthly',
|
|
|
|
|
'label' => '당월 미수금',
|
|
|
|
|
'amount' => (int) $currentMonthReceivables,
|
|
|
|
|
'sub_items' => [
|
|
|
|
|
['label' => '매출', 'value' => (int) $currentMonthSales],
|
|
|
|
|
['label' => '입금', 'value' => (int) $currentMonthDeposits],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 'rv_vendors',
|
|
|
|
|
'label' => '미수금 거래처',
|
|
|
|
|
'amount' => $vendorCount,
|
|
|
|
|
'unit' => '건',
|
|
|
|
|
'subLabel' => "연체 {$overdueVendorCount}건" . ($badDebtCount > 0 ? " · 악성채권 {$badDebtCount}건" : ''),
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 'rv_top3',
|
|
|
|
|
'label' => '미수금 Top 3',
|
|
|
|
|
'amount' => ! empty($topVendors) ? (int) $topVendors[0]['amount'] : 0,
|
|
|
|
|
'top_items' => $topVendors,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 체크포인트 생성
|
|
|
|
|
$checkPoints = $this->generateSummaryCheckPoints(
|
|
|
|
|
$tenantId,
|
|
|
|
|
$totalReceivables,
|
|
|
|
|
$overdueVendorCount,
|
|
|
|
|
$topVendors,
|
|
|
|
|
$vendorCount
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-26 15:47:14 +09:00
|
|
|
return [
|
2026-03-04 14:21:05 +09:00
|
|
|
'cards' => $cards,
|
|
|
|
|
'check_points' => $checkPoints,
|
2025-12-26 15:47:14 +09:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 14:21:05 +09:00
|
|
|
/**
|
|
|
|
|
* 악성채권 건수 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getBadDebtCount(int $tenantId): int
|
|
|
|
|
{
|
|
|
|
|
// bad_debts 테이블이 존재하면 사용, 없으면 0
|
|
|
|
|
try {
|
|
|
|
|
return \DB::table('bad_debts')
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->whereIn('status', ['collecting', 'legal_action'])
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
->count();
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 미수금 Top N 거래처 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getTopReceivableVendors(int $tenantId, int $limit = 3): array
|
|
|
|
|
{
|
|
|
|
|
$salesSub = \DB::table('sales')
|
|
|
|
|
->select('client_id', \DB::raw('SUM(total_amount) as total'))
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
2026-03-06 10:59:16 +09:00
|
|
|
->whereNull('deleted_at')
|
2026-03-04 14:21:05 +09:00
|
|
|
->groupBy('client_id');
|
|
|
|
|
|
|
|
|
|
$depositsSub = \DB::table('deposits')
|
|
|
|
|
->select('client_id', \DB::raw('SUM(amount) as total'))
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
2026-03-06 10:59:16 +09:00
|
|
|
->whereNull('deleted_at')
|
2026-03-04 14:21:05 +09:00
|
|
|
->groupBy('client_id');
|
|
|
|
|
|
|
|
|
|
$billsSub = \DB::table('bills')
|
|
|
|
|
->select('client_id', \DB::raw('SUM(amount) as total'))
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
2026-03-06 10:59:16 +09:00
|
|
|
->whereNull('deleted_at')
|
2026-03-04 14:21:05 +09:00
|
|
|
->where('bill_type', 'received')
|
|
|
|
|
->groupBy('client_id');
|
|
|
|
|
|
|
|
|
|
$results = \DB::table('clients as c')
|
|
|
|
|
->leftJoinSub($salesSub, 's', 'c.id', '=', 's.client_id')
|
|
|
|
|
->leftJoinSub($depositsSub, 'd', 'c.id', '=', 'd.client_id')
|
|
|
|
|
->leftJoinSub($billsSub, 'b', 'c.id', '=', 'b.client_id')
|
|
|
|
|
->select(
|
|
|
|
|
'c.name',
|
|
|
|
|
\DB::raw('(COALESCE(s.total, 0) - COALESCE(d.total, 0) - COALESCE(b.total, 0)) as receivable')
|
|
|
|
|
)
|
|
|
|
|
->where('c.tenant_id', $tenantId)
|
|
|
|
|
->where('c.is_active', true)
|
|
|
|
|
->having('receivable', '>', 0)
|
|
|
|
|
->orderByDesc('receivable')
|
|
|
|
|
->limit($limit)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
return $results->map(fn ($v) => [
|
|
|
|
|
'name' => $v->name,
|
|
|
|
|
'amount' => (int) $v->receivable,
|
|
|
|
|
])->toArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 대시보드 요약 체크포인트 생성
|
|
|
|
|
*/
|
|
|
|
|
private function generateSummaryCheckPoints(
|
|
|
|
|
int $tenantId,
|
|
|
|
|
float $totalReceivables,
|
|
|
|
|
int $overdueVendorCount,
|
|
|
|
|
array $topVendors,
|
|
|
|
|
int $vendorCount
|
|
|
|
|
): array {
|
|
|
|
|
$checkPoints = [];
|
|
|
|
|
|
|
|
|
|
// 연체 거래처 경고
|
|
|
|
|
if ($overdueVendorCount > 0) {
|
|
|
|
|
$checkPoints[] = [
|
|
|
|
|
'id' => 'rv_cp_overdue',
|
|
|
|
|
'type' => 'warning',
|
|
|
|
|
'message' => "연체 거래처 {$overdueVendorCount}곳. 회수 조치가 필요합니다.",
|
|
|
|
|
'highlights' => [
|
|
|
|
|
['text' => "연체 거래처 {$overdueVendorCount}곳", 'color' => 'red'],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 90일 이상 장기 미수금 체크
|
|
|
|
|
$longTermCount = $this->getLongTermReceivableCount($tenantId, 90);
|
|
|
|
|
if ($longTermCount > 0) {
|
|
|
|
|
$checkPoints[] = [
|
|
|
|
|
'id' => 'rv_cp_longterm',
|
|
|
|
|
'type' => 'error',
|
|
|
|
|
'message' => "90일 이상 장기 미수금 {$longTermCount}건 감지. 악성채권 전환 위험이 있습니다.",
|
|
|
|
|
'highlights' => [
|
|
|
|
|
['text' => "90일 이상 장기 미수금 {$longTermCount}건", 'color' => 'red'],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Top1 거래처 집중도 경고
|
|
|
|
|
if (! empty($topVendors) && $totalReceivables > 0) {
|
|
|
|
|
$top1Ratio = round(($topVendors[0]['amount'] / $totalReceivables) * 100);
|
|
|
|
|
if ($top1Ratio >= 50) {
|
|
|
|
|
$checkPoints[] = [
|
|
|
|
|
'id' => 'rv_cp_concentration',
|
|
|
|
|
'type' => 'warning',
|
|
|
|
|
'message' => "{$topVendors[0]['name']} 미수금이 전체의 {$top1Ratio}%를 차지합니다. 리스크 분산이 필요합니다.",
|
|
|
|
|
'highlights' => [
|
|
|
|
|
['text' => "{$topVendors[0]['name']}", 'color' => 'orange'],
|
|
|
|
|
['text' => "전체의 {$top1Ratio}%", 'color' => 'orange'],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 정상 상태 메시지
|
|
|
|
|
if (empty($checkPoints)) {
|
|
|
|
|
$totalFormatted = number_format($totalReceivables / 10000);
|
|
|
|
|
$checkPoints[] = [
|
|
|
|
|
'id' => 'rv_cp_normal',
|
|
|
|
|
'type' => 'success',
|
|
|
|
|
'message' => "총 미수금 {$totalFormatted}만원. 정상적으로 관리되고 있습니다.",
|
|
|
|
|
'highlights' => [
|
|
|
|
|
['text' => "{$totalFormatted}만원", 'color' => 'green'],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $checkPoints;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* N일 이상 장기 미수금 거래처 수 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getLongTermReceivableCount(int $tenantId, int $days): int
|
|
|
|
|
{
|
|
|
|
|
$cutoffDate = Carbon::now()->subDays($days)->format('Y-m-d');
|
|
|
|
|
|
|
|
|
|
// 연체 상태이면서 오래된 매출이 있는 거래처 수
|
|
|
|
|
$clientIds = Sale::where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->where('sale_date', '<=', $cutoffDate)
|
|
|
|
|
->distinct()
|
|
|
|
|
->pluck('client_id');
|
|
|
|
|
|
|
|
|
|
return Client::where('tenant_id', $tenantId)
|
|
|
|
|
->where('is_active', true)
|
|
|
|
|
->where('is_overdue', true)
|
|
|
|
|
->whereIn('id', $clientIds)
|
|
|
|
|
->count();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 15:47:14 +09:00
|
|
|
/**
|
2026-01-02 14:47:51 +09:00
|
|
|
* 월 기간 배열 생성
|
2026-01-13 19:49:06 +09:00
|
|
|
*
|
2026-01-02 14:47:51 +09:00
|
|
|
* @return array [['start' => 'Y-m-d', 'end' => 'Y-m-d', 'label' => 'YY.MM', 'year' => Y, 'month' => M], ...]
|
2025-12-26 15:47:14 +09:00
|
|
|
*/
|
2026-01-02 14:47:51 +09:00
|
|
|
private function generateMonthPeriods(bool $recentYear, string $year): array
|
2025-12-26 15:47:14 +09:00
|
|
|
{
|
2026-01-02 14:47:51 +09:00
|
|
|
$periods = [];
|
|
|
|
|
|
|
|
|
|
if ($recentYear) {
|
|
|
|
|
// 최근 1년: 현재 월 기준으로 12개월 전부터
|
|
|
|
|
$current = Carbon::now()->startOfMonth();
|
|
|
|
|
$start = $current->copy()->subMonths(11);
|
|
|
|
|
|
|
|
|
|
for ($i = 0; $i < 12; $i++) {
|
|
|
|
|
$month = $start->copy()->addMonths($i);
|
|
|
|
|
$periods[] = [
|
|
|
|
|
'start' => $month->format('Y-m-01'),
|
|
|
|
|
'end' => $month->endOfMonth()->format('Y-m-d'),
|
|
|
|
|
'label' => $month->format('y.m'),
|
|
|
|
|
'year' => (int) $month->format('Y'),
|
|
|
|
|
'month' => (int) $month->format('n'),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 특정 연도: 1월~12월
|
|
|
|
|
for ($month = 1; $month <= 12; $month++) {
|
|
|
|
|
$date = Carbon::createFromDate($year, $month, 1);
|
|
|
|
|
$periods[] = [
|
|
|
|
|
'start' => $date->format('Y-m-01'),
|
|
|
|
|
'end' => $date->endOfMonth()->format('Y-m-d'),
|
|
|
|
|
'label' => "{$month}월",
|
|
|
|
|
'year' => (int) $year,
|
|
|
|
|
'month' => $month,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $periods;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 이월잔액 계산 (기준일 이전까지의 누적 미수금)
|
|
|
|
|
*/
|
|
|
|
|
private function getCarryForwardBalance(int $tenantId, int $clientId, string $beforeDate): float
|
|
|
|
|
{
|
|
|
|
|
// 기준일 이전 총 매출
|
|
|
|
|
$totalSales = Sale::where('tenant_id', $tenantId)
|
|
|
|
|
->where('client_id', $clientId)
|
|
|
|
|
->where('sale_date', '<=', $beforeDate)
|
|
|
|
|
->sum('total_amount');
|
2025-12-26 15:47:14 +09:00
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
// 기준일 이전 총 입금
|
|
|
|
|
$totalDeposits = Deposit::where('tenant_id', $tenantId)
|
2025-12-26 15:47:14 +09:00
|
|
|
->where('client_id', $clientId)
|
2026-01-02 14:47:51 +09:00
|
|
|
->where('deposit_date', '<=', $beforeDate)
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
// 기준일 이전 총 어음
|
|
|
|
|
$totalBills = Bill::where('tenant_id', $tenantId)
|
|
|
|
|
->where('client_id', $clientId)
|
|
|
|
|
->where('bill_type', 'received')
|
|
|
|
|
->where('issue_date', '<=', $beforeDate)
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
return (float) ($totalSales - $totalDeposits - $totalBills);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 전체 거래처 이월잔액 합계
|
|
|
|
|
*/
|
|
|
|
|
private function getTotalCarryForwardBalance(int $tenantId, string $beforeDate): float
|
|
|
|
|
{
|
|
|
|
|
$totalSales = Sale::where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->where('sale_date', '<=', $beforeDate)
|
|
|
|
|
->sum('total_amount');
|
|
|
|
|
|
|
|
|
|
$totalDeposits = Deposit::where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->where('deposit_date', '<=', $beforeDate)
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
$totalBills = Bill::where('tenant_id', $tenantId)
|
|
|
|
|
->whereNotNull('client_id')
|
|
|
|
|
->where('bill_type', 'received')
|
|
|
|
|
->where('issue_date', '<=', $beforeDate)
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
return (float) ($totalSales - $totalDeposits - $totalBills);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 기간별 매출 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getSalesByPeriods(int $tenantId, int $clientId, array $periods): array
|
|
|
|
|
{
|
|
|
|
|
$result = [];
|
|
|
|
|
|
|
|
|
|
foreach ($periods as $index => $period) {
|
|
|
|
|
$total = Sale::where('tenant_id', $tenantId)
|
|
|
|
|
->where('client_id', $clientId)
|
|
|
|
|
->whereBetween('sale_date', [$period['start'], $period['end']])
|
|
|
|
|
->sum('total_amount');
|
|
|
|
|
|
|
|
|
|
$result[$index] = (float) $total;
|
2025-12-26 15:47:14 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-02 14:47:51 +09:00
|
|
|
* 기간별 입금 조회
|
2025-12-26 15:47:14 +09:00
|
|
|
*/
|
2026-01-02 14:47:51 +09:00
|
|
|
private function getDepositsByPeriods(int $tenantId, int $clientId, array $periods): array
|
2025-12-26 15:47:14 +09:00
|
|
|
{
|
|
|
|
|
$result = [];
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
foreach ($periods as $index => $period) {
|
|
|
|
|
$total = Deposit::where('tenant_id', $tenantId)
|
|
|
|
|
->where('client_id', $clientId)
|
|
|
|
|
->whereBetween('deposit_date', [$period['start'], $period['end']])
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
$result[$index] = (float) $total;
|
2025-12-26 15:47:14 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-02 14:47:51 +09:00
|
|
|
* 기간별 어음 조회
|
2025-12-26 15:47:14 +09:00
|
|
|
*/
|
2026-01-02 14:47:51 +09:00
|
|
|
private function getBillsByPeriods(int $tenantId, int $clientId, array $periods): array
|
2025-12-26 15:47:14 +09:00
|
|
|
{
|
|
|
|
|
$result = [];
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
foreach ($periods as $index => $period) {
|
|
|
|
|
$total = Bill::where('tenant_id', $tenantId)
|
|
|
|
|
->where('client_id', $clientId)
|
|
|
|
|
->where('bill_type', 'received')
|
|
|
|
|
->whereBetween('issue_date', [$period['start'], $period['end']])
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
$result[$index] = (float) $total;
|
2025-12-26 15:47:14 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-02 14:47:51 +09:00
|
|
|
* 누적 미수금 계산
|
|
|
|
|
* 1월: 이월잔액 + 1월 매출 - 1월 입금 - 1월 어음
|
|
|
|
|
* 2월: 1월 미수금 + 2월 매출 - 2월 입금 - 2월 어음
|
|
|
|
|
* ...
|
2025-12-26 15:47:14 +09:00
|
|
|
*/
|
2026-01-02 14:47:51 +09:00
|
|
|
private function calculateCumulativeReceivables(
|
|
|
|
|
float $carryForward,
|
|
|
|
|
array $sales,
|
|
|
|
|
array $deposits,
|
|
|
|
|
array $bills,
|
|
|
|
|
int $periodCount
|
|
|
|
|
): array {
|
2025-12-26 15:47:14 +09:00
|
|
|
$result = [];
|
2026-01-02 14:47:51 +09:00
|
|
|
$cumulative = $carryForward;
|
2025-12-26 15:47:14 +09:00
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
for ($i = 0; $i < $periodCount; $i++) {
|
|
|
|
|
$monthSales = $sales[$i] ?? 0;
|
|
|
|
|
$monthDeposits = $deposits[$i] ?? 0;
|
|
|
|
|
$monthBills = $bills[$i] ?? 0;
|
2025-12-26 15:47:14 +09:00
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
$cumulative = $cumulative + $monthSales - $monthDeposits - $monthBills;
|
|
|
|
|
$result[$i] = $cumulative;
|
2025-12-26 15:47:14 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-02 14:47:51 +09:00
|
|
|
* 기간별 금액을 프론트엔드 형식으로 변환 (매출, 입금, 어음용)
|
2025-12-26 15:47:14 +09:00
|
|
|
*/
|
2026-01-02 14:47:51 +09:00
|
|
|
private function formatPeriodAmounts(array $periodData, int $periodCount): array
|
2025-12-26 15:47:14 +09:00
|
|
|
{
|
|
|
|
|
$amounts = [];
|
|
|
|
|
$total = 0;
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
for ($i = 0; $i < $periodCount; $i++) {
|
|
|
|
|
$amount = $periodData[$i] ?? 0;
|
|
|
|
|
$amounts[] = $amount;
|
2025-12-26 15:47:14 +09:00
|
|
|
$total += $amount;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
return [
|
|
|
|
|
'values' => $amounts,
|
|
|
|
|
'total' => $total,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 미수금 금액을 프론트엔드 형식으로 변환 (누적이므로 total = 마지막 값)
|
|
|
|
|
*/
|
|
|
|
|
private function formatReceivableAmounts(array $receivables): array
|
|
|
|
|
{
|
|
|
|
|
$values = array_values($receivables);
|
|
|
|
|
$total = ! empty($values) ? end($values) : 0;
|
2025-12-26 15:47:14 +09:00
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
return [
|
|
|
|
|
'values' => $values,
|
|
|
|
|
'total' => $total,
|
|
|
|
|
];
|
2025-12-26 15:47:14 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-02 14:47:51 +09:00
|
|
|
* 연체 상태 일괄 업데이트
|
2025-12-26 15:47:14 +09:00
|
|
|
*/
|
2026-01-02 14:47:51 +09:00
|
|
|
public function updateOverdueStatus(array $updates): int
|
2025-12-26 15:47:14 +09:00
|
|
|
{
|
2026-01-02 14:47:51 +09:00
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$updatedCount = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($updates as $update) {
|
|
|
|
|
$clientId = (int) $update['id'];
|
|
|
|
|
$isOverdue = (bool) $update['is_overdue'];
|
|
|
|
|
|
|
|
|
|
$affected = Client::where('tenant_id', $tenantId)
|
|
|
|
|
->where('id', $clientId)
|
|
|
|
|
->update(['is_overdue' => $isOverdue]);
|
2025-12-26 15:47:14 +09:00
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
$updatedCount += $affected;
|
2025-12-26 15:47:14 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
return $updatedCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 거래처 메모 업데이트
|
|
|
|
|
*/
|
|
|
|
|
public function updateMemo(int $clientId, string $memo): bool
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
|
|
|
|
|
$affected = Client::where('tenant_id', $tenantId)
|
|
|
|
|
->where('id', $clientId)
|
|
|
|
|
->update(['memo' => $memo]);
|
|
|
|
|
|
|
|
|
|
return $affected > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 거래처 메모 일괄 업데이트
|
|
|
|
|
*/
|
|
|
|
|
public function updateMemos(array $memos): int
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$updatedCount = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($memos as $item) {
|
|
|
|
|
$clientId = (int) $item['id'];
|
|
|
|
|
$memo = $item['memo'] ?? '';
|
|
|
|
|
|
|
|
|
|
$affected = Client::where('tenant_id', $tenantId)
|
|
|
|
|
->where('id', $clientId)
|
|
|
|
|
->update(['memo' => $memo]);
|
|
|
|
|
|
|
|
|
|
$updatedCount += $affected;
|
|
|
|
|
}
|
2025-12-26 15:47:14 +09:00
|
|
|
|
2026-01-02 14:47:51 +09:00
|
|
|
return $updatedCount;
|
2025-12-26 15:47:14 +09:00
|
|
|
}
|
2026-01-13 19:49:06 +09:00
|
|
|
}
|