tenantId(); $query = Receiving::query() ->with(['creator:id,name', 'item:id,item_type,code,name']) ->where('tenant_id', $tenantId); // 검색어 필터 if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('order_no', 'like', "%{$search}%") ->orWhere('item_code', 'like', "%{$search}%") ->orWhere('item_name', 'like', "%{$search}%") ->orWhere('supplier', 'like', "%{$search}%"); }); } // 상태 필터 if (! empty($params['status'])) { if ($params['status'] === 'receiving_pending') { // 입고대기: receiving_pending + inspection_pending $query->whereIn('status', ['receiving_pending', 'inspection_pending']); } elseif ($params['status'] === 'completed') { $query->where('status', 'completed'); } else { $query->where('status', $params['status']); } } // 날짜 범위 필터 (작성일 기준) if (! empty($params['start_date'])) { $query->whereDate('created_at', '>=', $params['start_date']); } if (! empty($params['end_date'])) { $query->whereDate('created_at', '<=', $params['end_date']); } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); // 페이지네이션 $perPage = $params['per_page'] ?? 20; $paginator = $query->paginate($perPage); // 수입검사 템플릿 연결 여부 계산 $itemIds = $paginator->pluck('item_id')->filter()->unique()->values()->toArray(); $itemsWithInspection = $this->getItemsWithInspectionTemplate($itemIds); // has_inspection_template 필드 추가 $paginator->getCollection()->transform(function ($receiving) use ($itemsWithInspection) { $receiving->has_inspection_template = $receiving->item_id ? in_array($receiving->item_id, $itemsWithInspection) : false; return $receiving; }); return $paginator; } /** * 수입검사 템플릿에 연결된 품목 ID 조회 * * DocumentService::resolve()와 동일한 조건 사용: * - category: 영문 코드('incoming_inspection'), 한글('수입검사'), 부분 매칭 모두 지원 * - linked_item_ids: int/string 타입 모두 매칭 */ private function getItemsWithInspectionTemplate(array $itemIds): array { if (empty($itemIds)) { return []; } $tenantId = $this->tenantId(); // DocumentService::resolve()와 동일한 category 매칭 조건 $categoryCode = 'incoming_inspection'; $categoryName = '수입검사'; $templates = DB::table('document_templates') ->where('tenant_id', $tenantId) ->where('is_active', true) ->whereNotNull('linked_item_ids') ->where(function ($q) use ($categoryCode, $categoryName) { $q->where('category', $categoryCode) ->orWhere('category', $categoryName) ->orWhere('category', 'LIKE', "%{$categoryName}%"); }) ->get(['linked_item_ids']); $linkedItemIds = []; foreach ($templates as $template) { $ids = json_decode($template->linked_item_ids, true) ?? []; // int/string 타입 모두 매칭되도록 정수로 통일 foreach ($ids as $id) { $linkedItemIds[] = (int) $id; } } // 요청된 품목 ID도 정수로 통일하여 교집합 $intItemIds = array_map('intval', $itemIds); return array_values(array_intersect($intItemIds, array_unique($linkedItemIds))); } /** * 입고 통계 조회 */ public function stats(): array { $tenantId = $this->tenantId(); $today = now()->toDateString(); $receivingPendingCount = Receiving::where('tenant_id', $tenantId) ->where('status', 'receiving_pending') ->count(); $shippingCount = Receiving::where('tenant_id', $tenantId) ->where('status', 'shipping') ->count(); $inspectionPendingCount = Receiving::where('tenant_id', $tenantId) ->where('status', 'inspection_pending') ->count(); $todayReceivingCount = Receiving::where('tenant_id', $tenantId) ->where('status', 'completed') ->whereDate('receiving_date', $today) ->count(); return [ 'receiving_pending_count' => $receivingPendingCount, 'shipping_count' => $shippingCount, 'inspection_pending_count' => $inspectionPendingCount, 'today_receiving_count' => $todayReceivingCount, ]; } /** * 입고 상세 조회 */ public function show(int $id): Receiving { $tenantId = $this->tenantId(); return Receiving::query() ->where('tenant_id', $tenantId) ->with(['creator:id,name', 'item:id,item_type,code,name']) ->findOrFail($id); } /** * 입고 등록 */ public function store(array $data): Receiving { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 입고번호 자동 생성 $receivingNumber = $this->generateReceivingNumber($tenantId); // item_id 조회 (전달되지 않은 경우 item_code로 조회) $itemId = $data['item_id'] ?? null; if (! $itemId && ! empty($data['item_code'])) { $itemId = $this->findItemIdByCode($tenantId, $data['item_code']); } $receiving = new Receiving; $receiving->tenant_id = $tenantId; $receiving->receiving_number = $receivingNumber; $receiving->order_no = $data['order_no'] ?? null; $receiving->order_date = $data['order_date'] ?? null; $receiving->item_id = $itemId; $receiving->item_code = $data['item_code']; $receiving->item_name = $data['item_name']; $receiving->specification = $data['specification'] ?? null; $receiving->supplier = $data['supplier']; $receiving->order_qty = $data['order_qty'] ?? null; $receiving->order_unit = $data['order_unit'] ?? 'EA'; $receiving->due_date = $data['due_date'] ?? null; $receiving->receiving_qty = $data['receiving_qty'] ?? null; $receiving->receiving_date = $data['receiving_date'] ?? null; $receiving->lot_no = $data['lot_no'] ?? $this->generateLotNo(); $receiving->status = $data['status'] ?? 'receiving_pending'; $receiving->remark = $data['remark'] ?? null; // options 필드 처리 (제조사, 수입검사 등 확장 필드) $receiving->options = $this->buildOptions($data); $receiving->created_by = $userId; $receiving->updated_by = $userId; $receiving->save(); return $receiving; }); } /** * 입고 수정 */ public function update(int $id, array $data): Receiving { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $receiving = Receiving::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $receiving->canEdit()) { throw new \Exception(__('error.receiving.cannot_edit')); } if (isset($data['order_no'])) { $receiving->order_no = $data['order_no']; } if (isset($data['order_date'])) { $receiving->order_date = $data['order_date']; } if (isset($data['item_code'])) { $receiving->item_code = $data['item_code']; // item_code 변경 시 item_id도 업데이트 if (! isset($data['item_id'])) { $receiving->item_id = $this->findItemIdByCode($tenantId, $data['item_code']); } } if (isset($data['item_id'])) { $receiving->item_id = $data['item_id']; } if (isset($data['item_name'])) { $receiving->item_name = $data['item_name']; } if (array_key_exists('specification', $data)) { $receiving->specification = $data['specification']; } if (isset($data['supplier'])) { $receiving->supplier = $data['supplier']; } if (isset($data['order_qty'])) { $receiving->order_qty = $data['order_qty']; } if (isset($data['order_unit'])) { $receiving->order_unit = $data['order_unit']; } if (array_key_exists('due_date', $data)) { $receiving->due_date = $data['due_date']; } if (array_key_exists('remark', $data)) { $receiving->remark = $data['remark']; } // 상태 변경 감지 $oldStatus = $receiving->status; $newStatus = $data['status'] ?? $oldStatus; $wasCompleted = $oldStatus === 'completed'; // 입고완료(completed) 상태로 신규 전환 $isCompletingReceiving = $newStatus === 'completed' && ! $wasCompleted; if ($isCompletingReceiving) { // 입고수량 설정 (없으면 발주수량 사용) $receiving->receiving_qty = $data['receiving_qty'] ?? $receiving->order_qty; $receiving->receiving_date = $data['receiving_date'] ?? now()->toDateString(); $receiving->lot_no = $data['lot_no'] ?? $this->generateLotNo(); $receiving->status = 'completed'; } else { // 일반 필드 업데이트 if (isset($data['receiving_qty'])) { $receiving->receiving_qty = $data['receiving_qty']; } if (isset($data['receiving_date'])) { $receiving->receiving_date = $data['receiving_date']; } if (isset($data['lot_no'])) { $receiving->lot_no = $data['lot_no']; } if (isset($data['status'])) { $receiving->status = $data['status']; } } // options 필드 업데이트 (제조사, 수입검사 등 확장 필드) $receiving->options = $this->mergeOptions($receiving->options, $data); $receiving->updated_by = $userId; $receiving->save(); // 재고 연동 if ($receiving->item_id) { $stockService = app(StockService::class); if ($isCompletingReceiving) { // 대기 → 완료: 전량 재고 증가 $stockService->increaseFromReceiving($receiving); } elseif ($wasCompleted) { // 기존 완료 상태에서 수정: 차이만큼 조정 // 완료→완료(수량변경): newQty = 변경된 수량 // 완료→대기: newQty = 0 (전량 차감) $newQty = $newStatus === 'completed' ? (float) $receiving->receiving_qty : 0; $stockService->adjustFromReceiving($receiving, $newQty); } } return $receiving->fresh(); }); } /** * 입고 삭제 */ public function destroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $receiving = Receiving::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $receiving->canDelete()) { throw new \Exception(__('error.receiving.cannot_delete')); } $receiving->deleted_by = $userId; $receiving->save(); $receiving->delete(); return true; }); } /** * 입고처리 (상태 변경 + 입고 정보 입력 + 재고 연동) */ public function process(int $id, array $data): Receiving { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $receiving = Receiving::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $receiving->canProcess()) { throw new \Exception(__('error.receiving.cannot_process')); } // LOT번호 생성 (없으면 자동 생성) $lotNo = $data['lot_no'] ?? $this->generateLotNo(); $receiving->receiving_qty = $data['receiving_qty']; $receiving->receiving_date = $data['receiving_date'] ?? now()->toDateString(); $receiving->lot_no = $lotNo; $receiving->supplier_lot = $data['supplier_lot'] ?? null; $receiving->receiving_location = $data['receiving_location'] ?? null; $receiving->receiving_manager = $data['receiving_manager'] ?? null; $receiving->status = 'completed'; $receiving->remark = $data['remark'] ?? $receiving->remark; $receiving->updated_by = $userId; $receiving->save(); // 🆕 재고 연동: Stock + StockLot 생성/갱신 if ($receiving->item_id) { app(StockService::class)->increaseFromReceiving($receiving); } return $receiving->fresh(); }); } /** * 입고번호 자동 생성 */ private function generateReceivingNumber(int $tenantId): string { $prefix = 'RV'.date('Ymd'); $lastReceiving = Receiving::query() ->where('tenant_id', $tenantId) ->where('receiving_number', 'like', $prefix.'%') ->orderBy('receiving_number', 'desc') ->first(); if ($lastReceiving) { $lastSeq = (int) substr($lastReceiving->receiving_number, -4); $newSeq = $lastSeq + 1; } else { $newSeq = 1; } return $prefix.str_pad($newSeq, 4, '0', STR_PAD_LEFT); } /** * LOT번호 자동 생성 */ private function generateLotNo(): string { $now = now(); $year = $now->format('y'); $month = $now->format('m'); $day = $now->format('d'); $seq = str_pad(rand(1, 99), 2, '0', STR_PAD_LEFT); return "{$year}{$month}{$day}-{$seq}"; } /** * 품목코드로 품목 ID 조회 */ private function findItemIdByCode(int $tenantId, string $itemCode): ?int { $item = DB::table('items') ->where('tenant_id', $tenantId) ->where('code', $itemCode) ->whereNull('deleted_at') ->first(['id']); return $item?->id; } /** * options 필드 빌드 (등록 시) */ private function buildOptions(array $data): ?array { $options = []; // 제조사 if (isset($data['manufacturer'])) { $options[Receiving::OPTION_MANUFACTURER] = $data['manufacturer']; } // 거래처 자재번호 if (isset($data['material_no'])) { $options[Receiving::OPTION_MATERIAL_NO] = $data['material_no']; } // 수입검사 상태 (적/부적/-) if (isset($data['inspection_status'])) { $options[Receiving::OPTION_INSPECTION_STATUS] = $data['inspection_status']; } // 검사일 if (isset($data['inspection_date'])) { $options[Receiving::OPTION_INSPECTION_DATE] = $data['inspection_date']; } // 검사결과 (합격/불합격) if (isset($data['inspection_result'])) { $options[Receiving::OPTION_INSPECTION_RESULT] = $data['inspection_result']; } // 추가 확장 필드가 있으면 여기에 계속 추가 가능 return ! empty($options) ? $options : null; } /** * options 필드 병합 (수정 시) */ private function mergeOptions(?array $existing, array $data): ?array { $options = $existing ?? []; // 제조사 if (array_key_exists('manufacturer', $data)) { if ($data['manufacturer'] === null || $data['manufacturer'] === '') { unset($options[Receiving::OPTION_MANUFACTURER]); } else { $options[Receiving::OPTION_MANUFACTURER] = $data['manufacturer']; } } // 거래처 자재번호 if (array_key_exists('material_no', $data)) { if ($data['material_no'] === null || $data['material_no'] === '') { unset($options[Receiving::OPTION_MATERIAL_NO]); } else { $options[Receiving::OPTION_MATERIAL_NO] = $data['material_no']; } } // 수입검사 상태 if (array_key_exists('inspection_status', $data)) { if ($data['inspection_status'] === null || $data['inspection_status'] === '') { unset($options[Receiving::OPTION_INSPECTION_STATUS]); } else { $options[Receiving::OPTION_INSPECTION_STATUS] = $data['inspection_status']; } } // 검사일 if (array_key_exists('inspection_date', $data)) { if ($data['inspection_date'] === null || $data['inspection_date'] === '') { unset($options[Receiving::OPTION_INSPECTION_DATE]); } else { $options[Receiving::OPTION_INSPECTION_DATE] = $data['inspection_date']; } } // 검사결과 if (array_key_exists('inspection_result', $data)) { if ($data['inspection_result'] === null || $data['inspection_result'] === '') { unset($options[Receiving::OPTION_INSPECTION_RESULT]); } else { $options[Receiving::OPTION_INSPECTION_RESULT] = $data['inspection_result']; } } return ! empty($options) ? $options : null; } }