feat(API): 부실채권, 재고, 입고 기능 개선
- BadDebt 컨트롤러/서비스 기능 확장 - StockService 재고 조회 로직 개선 - ProcessReceivingRequest 검증 규칙 수정 - Item, Order, CommonCode, Shipment 모델 업데이트 - TodayIssueObserverService 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-01-23 10:19:57
|
||||
> **자동 생성**: 2026-01-23 15:57:29
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
|
||||
@@ -18,14 +18,15 @@ public function __construct(
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 악성채권 목록
|
||||
* 악성채권 목록 (거래처 기준)
|
||||
*
|
||||
* 거래처별 활성 악성채권 집계 목록을 반환
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'client_id',
|
||||
'status',
|
||||
'is_active',
|
||||
'search',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
@@ -33,9 +34,54 @@ public function index(Request $request)
|
||||
'page',
|
||||
]);
|
||||
|
||||
$badDebts = $this->service->index($params);
|
||||
$clients = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($badDebts, __('message.fetched'));
|
||||
// 거래처 기준 응답으로 변환
|
||||
$transformedData = $clients->through(function ($client) {
|
||||
// 가장 최근 활성 악성채권 정보
|
||||
$latestBadDebt = $client->activeBadDebts->first();
|
||||
|
||||
return [
|
||||
'id' => $client->id,
|
||||
'client_id' => $client->id,
|
||||
'client_code' => $client->client_code,
|
||||
'client_name' => $client->name,
|
||||
'business_no' => $client->business_no,
|
||||
'contact_person' => $client->contact_person,
|
||||
'phone' => $client->phone,
|
||||
'mobile' => $client->mobile,
|
||||
'email' => $client->email,
|
||||
'address' => $client->address,
|
||||
'client_type' => $client->client_type,
|
||||
// 집계 데이터
|
||||
'total_debt_amount' => (float) ($client->total_debt_amount ?? 0),
|
||||
'max_overdue_days' => (int) ($client->max_overdue_days ?? 0),
|
||||
'bad_debt_count' => (int) ($client->active_bad_debt_count ?? 0),
|
||||
// 대표 상태 (가장 최근 악성채권의 상태)
|
||||
'status' => $latestBadDebt?->status ?? 'collecting',
|
||||
'is_active' => true, // 리스트에 나온 건 모두 활성
|
||||
// 담당자 (가장 최근 악성채권의 담당자)
|
||||
'assigned_user' => $latestBadDebt?->assignedUser ? [
|
||||
'id' => $latestBadDebt->assignedUser->id,
|
||||
'name' => $latestBadDebt->assignedUser->name,
|
||||
] : null,
|
||||
// 개별 악성채권 목록
|
||||
'bad_debts' => $client->activeBadDebts->map(fn ($bd) => [
|
||||
'id' => $bd->id,
|
||||
'debt_amount' => (float) $bd->debt_amount,
|
||||
'status' => $bd->status,
|
||||
'overdue_days' => $bd->overdue_days,
|
||||
'is_active' => $bd->is_active,
|
||||
'occurred_at' => $bd->occurred_at?->toDateString(),
|
||||
'assigned_user' => $bd->assignedUser ? [
|
||||
'id' => $bd->assignedUser->id,
|
||||
'name' => $bd->assignedUser->name,
|
||||
] : null,
|
||||
])->toArray(),
|
||||
];
|
||||
});
|
||||
|
||||
return ApiResponse::success($transformedData, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ public function rules(): array
|
||||
'receiving_date' => ['nullable', 'date'],
|
||||
'lot_no' => ['nullable', 'string', 'max:50'],
|
||||
'supplier_lot' => ['nullable', 'string', 'max:50'],
|
||||
'receiving_location' => ['required', 'string', 'max:100'],
|
||||
'receiving_location' => ['nullable', 'string', 'max:100'],
|
||||
'receiving_manager' => ['nullable', 'string', 'max:50'],
|
||||
'remark' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
@@ -30,7 +30,6 @@ public function messages(): array
|
||||
'receiving_qty.required' => __('validation.required', ['attribute' => '입고수량']),
|
||||
'receiving_qty.numeric' => __('validation.numeric', ['attribute' => '입고수량']),
|
||||
'receiving_qty.min' => __('validation.min.numeric', ['attribute' => '입고수량', 'min' => 0]),
|
||||
'receiving_location.required' => __('validation.required', ['attribute' => '입고위치']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,14 @@ public function details()
|
||||
return $this->hasOne(ItemDetail::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 정보 (1:1)
|
||||
*/
|
||||
public function stock()
|
||||
{
|
||||
return $this->hasOne(\App\Models\Tenants\Stock::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리
|
||||
*/
|
||||
@@ -130,26 +138,29 @@ public function tags()
|
||||
|
||||
/**
|
||||
* 특정 타입 필터
|
||||
* join 시 ambiguous 에러 방지를 위해 테이블 prefix 사용
|
||||
*/
|
||||
public function scopeType($query, string $type)
|
||||
{
|
||||
return $query->where('item_type', strtoupper($type));
|
||||
return $query->where('items.item_type', strtoupper($type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Products 타입만 (FG, PT)
|
||||
* join 시 ambiguous 에러 방지를 위해 테이블 prefix 사용
|
||||
*/
|
||||
public function scopeProducts($query)
|
||||
{
|
||||
return $query->whereIn('item_type', self::PRODUCT_TYPES);
|
||||
return $query->whereIn('items.item_type', self::PRODUCT_TYPES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Materials 타입만 (SM, RM, CS)
|
||||
* join 시 ambiguous 에러 방지를 위해 테이블 prefix 사용
|
||||
*/
|
||||
public function scopeMaterials($query)
|
||||
{
|
||||
return $query->whereIn('item_type', self::MATERIAL_TYPES);
|
||||
return $query->whereIn('items.item_type', self::MATERIAL_TYPES);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\Production\WorkOrder;
|
||||
use App\Models\Products\CommonCode;
|
||||
use App\Models\Quote\Quote;
|
||||
use App\Models\Tenants\Sale;
|
||||
use App\Models\Tenants\Shipment;
|
||||
@@ -144,6 +145,13 @@ class Order extends Model
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* JSON 응답에 자동 포함할 accessor
|
||||
*/
|
||||
protected $appends = [
|
||||
'delivery_method_label',
|
||||
];
|
||||
|
||||
/**
|
||||
* 수주 상세 품목
|
||||
*/
|
||||
@@ -240,6 +248,14 @@ public function getSalesRecognitionLabelAttribute(): string
|
||||
return self::SALES_RECOGNITION_TYPES[$this->sales_recognition] ?? '출하완료 시';
|
||||
}
|
||||
|
||||
/**
|
||||
* 배송방식 라벨 (common_codes 테이블에서 조회)
|
||||
*/
|
||||
public function getDeliveryMethodLabelAttribute(): string
|
||||
{
|
||||
return CommonCode::getLabel('delivery_method', $this->delivery_method_code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주확정 시 매출 생성 여부
|
||||
*/
|
||||
|
||||
@@ -44,4 +44,42 @@ public function children()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 그룹과 코드로 라벨(name) 조회
|
||||
*
|
||||
* @param string $codeGroup 코드 그룹 (예: 'delivery_method')
|
||||
* @param string|null $code 코드값 (예: 'direct')
|
||||
* @return string 라벨 (없으면 코드값 반환)
|
||||
*/
|
||||
public static function getLabel(string $codeGroup, ?string $code): string
|
||||
{
|
||||
if (empty($code)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$commonCode = static::withoutGlobalScopes()
|
||||
->where('code_group', $codeGroup)
|
||||
->where('code', $code)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
return $commonCode?->name ?? $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 그룹의 전체 코드 목록 조회 (code => name 배열)
|
||||
*
|
||||
* @param string $codeGroup 코드 그룹
|
||||
* @return array<string, string> [code => name] 형태의 배열
|
||||
*/
|
||||
public static function getCodeMap(string $codeGroup): array
|
||||
{
|
||||
return static::withoutGlobalScopes()
|
||||
->where('code_group', $codeGroup)
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->pluck('name', 'code')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Production\WorkOrder;
|
||||
use App\Models\Products\CommonCode;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -77,6 +78,7 @@ class Shipment extends Model
|
||||
*/
|
||||
protected $appends = [
|
||||
'order_info',
|
||||
'delivery_method_label',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -172,11 +174,11 @@ public function getPriorityLabelAttribute(): string
|
||||
}
|
||||
|
||||
/**
|
||||
* 배송방식 라벨
|
||||
* 배송방식 라벨 (common_codes 테이블에서 조회)
|
||||
*/
|
||||
public function getDeliveryMethodLabelAttribute(): string
|
||||
{
|
||||
return self::DELIVERY_METHODS[$this->delivery_method] ?? $this->delivery_method;
|
||||
return CommonCode::getLabel('delivery_method', $this->delivery_method);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,50 +5,69 @@
|
||||
use App\Models\BadDebts\BadDebt;
|
||||
use App\Models\BadDebts\BadDebtDocument;
|
||||
use App\Models\BadDebts\BadDebtMemo;
|
||||
use App\Models\Orders\Client;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BadDebtService extends Service
|
||||
{
|
||||
/**
|
||||
* 악성채권 목록 조회
|
||||
* 악성채권 목록 조회 (거래처 기준)
|
||||
*
|
||||
* 거래처별로 is_active=true인 악성채권을 집계하여 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = BadDebt::query()
|
||||
$query = Client::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['client:id,name,client_code', 'assignedUser:id,name']);
|
||||
// is_active=true인 악성채권이 있는 거래처만
|
||||
->whereHas('badDebts', function ($q) {
|
||||
$q->where('is_active', true);
|
||||
})
|
||||
// 활성 악성채권 eager loading (추심중/법적조치 + is_active=true)
|
||||
->with(['activeBadDebts' => function ($q) {
|
||||
$q->with('assignedUser:id,name')
|
||||
->orderBy('created_at', 'desc');
|
||||
}])
|
||||
// 집계: 총 미수금액 (is_active=true인 건만)
|
||||
->withSum(['badDebts as total_debt_amount' => function ($q) {
|
||||
$q->where('is_active', true);
|
||||
}], 'debt_amount')
|
||||
// 집계: 최대 연체일수 (is_active=true인 건만)
|
||||
->withMax(['badDebts as max_overdue_days' => function ($q) {
|
||||
$q->where('is_active', true);
|
||||
}], 'overdue_days')
|
||||
// 집계: 악성채권 건수 (is_active=true인 건만)
|
||||
->withCount(['badDebts as active_bad_debt_count' => function ($q) {
|
||||
$q->where('is_active', true);
|
||||
}]);
|
||||
|
||||
// 거래처 필터
|
||||
if (! empty($params['client_id'])) {
|
||||
$query->where('client_id', $params['client_id']);
|
||||
$query->where('id', $params['client_id']);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
// 상태 필터 (해당 상태의 악성채권이 있는 거래처만)
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 활성화 필터
|
||||
if (isset($params['is_active'])) {
|
||||
$query->where('is_active', $params['is_active']);
|
||||
$query->whereHas('badDebts', function ($q) use ($params) {
|
||||
$q->where('is_active', true)
|
||||
->where('status', $params['status']);
|
||||
});
|
||||
}
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->whereHas('client', function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('client_code', 'like', "%{$search}%");
|
||||
});
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('client_code', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortBy = $params['sort_by'] ?? 'total_debt_amount';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
@@ -59,14 +78,16 @@ public function index(array $params): LengthAwarePaginator
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 요약 통계
|
||||
* 악성채권 요약 통계 (is_active=true인 건만)
|
||||
*/
|
||||
public function summary(array $params = []): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// is_active=true인 악성채권만 통계
|
||||
$query = BadDebt::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('is_active', true);
|
||||
|
||||
// 거래처 필터
|
||||
if (! empty($params['client_id'])) {
|
||||
@@ -82,12 +103,20 @@ public function summary(array $params = []): array
|
||||
$recoveredAmount = (clone $query)->recovered()->sum('debt_amount');
|
||||
$badDebtAmount = (clone $query)->badDebt()->sum('debt_amount');
|
||||
|
||||
// 거래처 수 (is_active=true인 악성채권이 있는)
|
||||
$clientCount = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('is_active', true)
|
||||
->distinct('client_id')
|
||||
->count('client_id');
|
||||
|
||||
return [
|
||||
'total_amount' => (float) $totalAmount,
|
||||
'collecting_amount' => (float) $collectingAmount,
|
||||
'legal_action_amount' => (float) $legalActionAmount,
|
||||
'recovered_amount' => (float) $recoveredAmount,
|
||||
'bad_debt_amount' => (float) $badDebtAmount,
|
||||
'client_count' => $clientCount,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,50 +2,77 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\Tenants\Stock;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class StockService extends Service
|
||||
{
|
||||
/**
|
||||
* 재고 목록 조회
|
||||
* Item 타입 → 재고관리 라벨 매핑
|
||||
*/
|
||||
public const ITEM_TYPE_LABELS = [
|
||||
'RM' => '원자재',
|
||||
'SM' => '부자재',
|
||||
'CS' => '소모품',
|
||||
];
|
||||
|
||||
/**
|
||||
* 재고 목록 조회 (Item 메인 + Stock LEFT JOIN)
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Stock::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('item');
|
||||
// Item 테이블이 메인 (materials 타입만: SM, RM, CS)
|
||||
$query = Item::query()
|
||||
->where('items.tenant_id', $tenantId)
|
||||
->materials() // SM, RM, CS만
|
||||
->with('stock');
|
||||
|
||||
// 검색어 필터
|
||||
// 검색어 필터 (Item 기준)
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('item_code', 'like', "%{$search}%")
|
||||
->orWhere('item_name', 'like', "%{$search}%");
|
||||
$q->where('items.code', 'like', "%{$search}%")
|
||||
->orWhere('items.name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 품목유형 필터
|
||||
// 품목유형 필터 (Item.item_type 기준: RM, SM, CS)
|
||||
if (! empty($params['item_type'])) {
|
||||
$query->where('item_type', $params['item_type']);
|
||||
$query->where('items.item_type', strtoupper($params['item_type']));
|
||||
}
|
||||
|
||||
// 재고 상태 필터
|
||||
// 재고 상태 필터 (Stock.status)
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
$query->whereHas('stock', function ($q) use ($params) {
|
||||
$q->where('status', $params['status']);
|
||||
});
|
||||
}
|
||||
|
||||
// 위치 필터
|
||||
// 위치 필터 (Stock.location)
|
||||
if (! empty($params['location'])) {
|
||||
$query->where('location', 'like', "%{$params['location']}%");
|
||||
$query->whereHas('stock', function ($q) use ($params) {
|
||||
$q->where('location', 'like', "%{$params['location']}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'item_code';
|
||||
$sortBy = $params['sort_by'] ?? 'code';
|
||||
$sortDir = $params['sort_dir'] ?? 'asc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// Item 테이블 기준 정렬 필드 매핑
|
||||
$sortMapping = [
|
||||
'item_code' => 'items.code',
|
||||
'item_name' => 'items.name',
|
||||
'item_type' => 'items.item_type',
|
||||
'code' => 'items.code',
|
||||
'name' => 'items.name',
|
||||
];
|
||||
|
||||
$sortColumn = $sortMapping[$sortBy] ?? 'items.code';
|
||||
$query->orderBy($sortColumn, $sortDir);
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
@@ -54,24 +81,43 @@ public function index(array $params): LengthAwarePaginator
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 통계 조회
|
||||
* 재고 통계 조회 (Item 기준)
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$totalItems = Stock::where('tenant_id', $tenantId)->count();
|
||||
|
||||
$normalCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'normal')
|
||||
// 전체 자재 품목 수 (Item 기준)
|
||||
$totalItems = Item::where('tenant_id', $tenantId)
|
||||
->materials()
|
||||
->count();
|
||||
|
||||
$lowCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'low')
|
||||
// 재고 상태별 카운트 (Stock이 있는 Item 기준)
|
||||
$normalCount = Item::where('items.tenant_id', $tenantId)
|
||||
->materials()
|
||||
->whereHas('stock', function ($q) {
|
||||
$q->where('status', 'normal');
|
||||
})
|
||||
->count();
|
||||
|
||||
$outCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'out')
|
||||
$lowCount = Item::where('items.tenant_id', $tenantId)
|
||||
->materials()
|
||||
->whereHas('stock', function ($q) {
|
||||
$q->where('status', 'low');
|
||||
})
|
||||
->count();
|
||||
|
||||
$outCount = Item::where('items.tenant_id', $tenantId)
|
||||
->materials()
|
||||
->whereHas('stock', function ($q) {
|
||||
$q->where('status', 'out');
|
||||
})
|
||||
->count();
|
||||
|
||||
// 재고 정보가 없는 Item 수
|
||||
$noStockCount = Item::where('items.tenant_id', $tenantId)
|
||||
->materials()
|
||||
->whereDoesntHave('stock')
|
||||
->count();
|
||||
|
||||
return [
|
||||
@@ -79,57 +125,73 @@ public function stats(): array
|
||||
'normal_count' => $normalCount,
|
||||
'low_count' => $lowCount,
|
||||
'out_count' => $outCount,
|
||||
'no_stock_count' => $noStockCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 상세 조회 (LOT 포함)
|
||||
* 재고 상세 조회 (Item 기준, LOT 포함)
|
||||
*/
|
||||
public function show(int $id): Stock
|
||||
public function show(int $id): Item
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Stock::query()
|
||||
return Item::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['item', 'lots' => function ($query) {
|
||||
->materials()
|
||||
->with(['stock.lots' => function ($query) {
|
||||
$query->orderBy('fifo_order');
|
||||
}])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목코드로 재고 조회
|
||||
* 품목코드로 재고 조회 (Item 기준)
|
||||
*/
|
||||
public function findByItemCode(string $itemCode): ?Stock
|
||||
public function findByItemCode(string $itemCode): ?Item
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Stock::query()
|
||||
return Item::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('item_code', $itemCode)
|
||||
->materials()
|
||||
->where('code', $itemCode)
|
||||
->with('stock')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목유형별 통계
|
||||
* 품목유형별 통계 (Item.item_type 기준)
|
||||
*/
|
||||
public function statsByItemType(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$stats = Stock::where('tenant_id', $tenantId)
|
||||
->selectRaw('item_type, COUNT(*) as count, SUM(stock_qty) as total_qty')
|
||||
// Item 기준으로 통계 (materials 타입만)
|
||||
$stats = Item::where('tenant_id', $tenantId)
|
||||
->materials()
|
||||
->selectRaw('item_type, COUNT(*) as count')
|
||||
->groupBy('item_type')
|
||||
->get()
|
||||
->keyBy('item_type');
|
||||
|
||||
// 재고 수량 합계 (Stock이 있는 경우)
|
||||
$stockQtys = Item::where('items.tenant_id', $tenantId)
|
||||
->materials()
|
||||
->join('stocks', 'items.id', '=', 'stocks.item_id')
|
||||
->selectRaw('items.item_type, SUM(stocks.stock_qty) as total_qty')
|
||||
->groupBy('items.item_type')
|
||||
->get()
|
||||
->keyBy('item_type');
|
||||
|
||||
$result = [];
|
||||
foreach (Stock::ITEM_TYPES as $key => $label) {
|
||||
$data = $stats->get($key);
|
||||
foreach (self::ITEM_TYPE_LABELS as $key => $label) {
|
||||
$itemData = $stats->get($key);
|
||||
$stockData = $stockQtys->get($key);
|
||||
$result[$key] = [
|
||||
'label' => $label,
|
||||
'count' => $data?->count ?? 0,
|
||||
'total_qty' => $data?->total_qty ?? 0,
|
||||
'count' => $itemData?->count ?? 0,
|
||||
'total_qty' => $stockData?->total_qty ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -87,14 +87,19 @@ public function handleOrderDeleted(Order $order): void
|
||||
}
|
||||
|
||||
/**
|
||||
* 미수금(주식 이슈) 생성/삭제
|
||||
* 미수금(추심 이슈) 생성/삭제
|
||||
*
|
||||
* is_active가 true로 설정(토글 ON)되면 오늘의 이슈에 추가
|
||||
* is_active가 false로 설정(토글 OFF)되면 오늘의 이슈에서 삭제
|
||||
*/
|
||||
public function handleBadDebtChange(BadDebt $badDebt): void
|
||||
{
|
||||
// 추심 진행 중인 건만 이슈 생성
|
||||
if (in_array($badDebt->status, ['in_progress', 'legal_action'])) {
|
||||
// is_active(설정 토글)이 켜져 있고, 추심중 또는 법적조치 상태인 경우 이슈 생성
|
||||
$isActiveStatus = in_array($badDebt->status, [BadDebt::STATUS_COLLECTING, BadDebt::STATUS_LEGAL_ACTION]);
|
||||
|
||||
if ($badDebt->is_active && $isActiveStatus) {
|
||||
$clientName = $badDebt->client?->name ?? __('message.today_issue.unknown_client');
|
||||
$amount = number_format($badDebt->total_amount ?? 0);
|
||||
$amount = number_format($badDebt->debt_amount ?? 0);
|
||||
$days = $badDebt->overdue_days ?? 0;
|
||||
|
||||
$this->createIssueWithFcm(
|
||||
@@ -112,6 +117,7 @@ public function handleBadDebtChange(BadDebt $badDebt): void
|
||||
expiresAt: null // 해결될 때까지 유지
|
||||
);
|
||||
} else {
|
||||
// is_active가 false이거나 상태가 회수완료/대손처리인 경우 이슈 삭제
|
||||
TodayIssue::removeBySource($badDebt->tenant_id, TodayIssue::SOURCE_BAD_DEBT, $badDebt->id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user