'purchased_part', 'PT' => 'bent_part', 'SM' => 'sub_material', 'RM' => 'raw_material', 'CS' => 'consumable', ]; /** 철강 관련 공급업체 (업체명 => 제조사) */ private const STEEL_SUPPLIERS = [ '(주)포스코' => '포스코', '현대제철(주)' => '현대제철', '동국제강(주)' => '동국제강', '(주)세아제강' => '세아제강', '한국철강(주)' => '한국철강', ]; /** 원단/기타 공급업체 */ private const FABRIC_SUPPLIERS = [ '(주)대한스틸' => '대한스틸', '(주)한국소재' => '한국소재', '삼화산업(주)' => '삼화산업', ]; /** 창고 위치 */ private const LOCATIONS = ['A-01-01', 'A-01-02', 'A-02-01', 'B-01-01', 'B-02-01', 'C-01-01']; public function handle(): int { $tenantId = (int) $this->option('tenant'); $date = $this->option('date'); $dryRun = $this->option('dry-run'); $this->info('=== 원자재 입고 테스트 데이터 생성 ==='); $this->info("테넌트: {$tenantId} | 일자: {$date}".($dryRun ? ' [DRY-RUN]' : '')); // Step 1: RM 품목 조회 $items = Item::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('item_type', 'RM') ->whereNull('deleted_at') ->orderBy('code') ->get(); if ($items->isEmpty()) { $this->error("테넌트 {$tenantId}에 원자재(RM) 품목이 없습니다."); return self::FAILURE; } $this->info("대상 품목: {$items->count()}건"); $this->newLine(); if ($dryRun) { $this->table( ['코드', '품목명', '단위', '예상 공급업체', '예상 수량'], $items->map(fn ($item) => [ $item->code, $item->name, $item->unit, $this->getSupplierForItem($item->name)[0], '10~100 (10단위)', ])->toArray() ); return self::SUCCESS; } // 채번 시퀀스 초기화 $datePrefix = date('Ymd', strtotime($date)); $dateShort = date('ymd', strtotime($date)); $receivingSeq = $this->getNextReceivingSeq($tenantId, $datePrefix); $lotSeq = $this->getNextLotSeq($tenantId, $dateShort); $inspectionSeq = $this->getNextInspectionSeq($tenantId, $dateShort); $orderSeq = 1; $created = ['receiving' => 0, 'stock' => 0, 'stock_lot' => 0, 'inspection' => 0]; DB::transaction(function () use ($items, $tenantId, $date, $datePrefix, $dateShort, &$receivingSeq, &$lotSeq, &$inspectionSeq, &$orderSeq, &$created) { foreach ($items as $item) { $qty = rand(1, 10) * 10; // 10~100 (10단위) [$supplierName, $manufacturer] = $this->getSupplierForItem($item->name); $location = self::LOCATIONS[array_rand(self::LOCATIONS)]; // Step 2: Receiving 생성 $receivingNumber = 'RV'.$datePrefix.str_pad($receivingSeq++, 4, '0', STR_PAD_LEFT); $lotNo = $dateShort.'-'.str_pad($lotSeq++, 2, '0', STR_PAD_LEFT); $orderNo = 'PO-'.$dateShort.'-'.str_pad($orderSeq++, 3, '0', STR_PAD_LEFT); $receiving = Receiving::withoutGlobalScopes()->create([ 'tenant_id' => $tenantId, 'receiving_number' => $receivingNumber, 'order_no' => $orderNo, 'order_date' => $date, 'item_id' => $item->id, 'item_code' => $item->code, 'item_name' => $item->name, 'supplier' => $supplierName, 'order_qty' => $qty, 'order_unit' => $item->unit ?: 'EA', 'due_date' => $date, 'receiving_qty' => $qty, 'receiving_date' => $date, 'lot_no' => $lotNo, 'supplier_lot' => 'SUP-'.rand(1000, 9999), 'receiving_location' => $location, 'receiving_manager' => '관리자', 'status' => 'completed', 'remark' => '테스트 데이터 (서비스 준비)', 'options' => [ 'manufacturer' => $manufacturer, 'inspection_status' => '적', 'inspection_date' => $date, 'inspection_result' => '합격', ], 'created_by' => self::USER_ID, 'updated_by' => self::USER_ID, ]); $created['receiving']++; // Step 3: Stock 생성/갱신 (item_id 또는 item_code로 조회) $stockType = self::ITEM_TYPE_MAP[$item->item_type] ?? 'raw_material'; $stock = Stock::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where(function ($q) use ($item) { $q->where('item_id', $item->id) ->orWhere('item_code', $item->code); }) ->first(); if ($stock) { $stock->stock_qty += $qty; $stock->available_qty += $qty; $stock->lot_count += 1; $stock->last_receipt_date = $date; $stock->status = 'normal'; $stock->updated_by = self::USER_ID; $stock->save(); } else { $stock = Stock::withoutGlobalScopes()->create([ 'tenant_id' => $tenantId, 'item_id' => $item->id, 'item_code' => $item->code, 'item_name' => $item->name, 'item_type' => $stockType, 'unit' => $item->unit ?: 'EA', 'stock_qty' => $qty, 'safety_stock' => 10, 'reserved_qty' => 0, 'available_qty' => $qty, 'lot_count' => 1, 'oldest_lot_date' => $date, 'location' => $location, 'status' => 'normal', 'last_receipt_date' => $date, 'created_by' => self::USER_ID, ]); $created['stock']++; } // Step 4: StockLot 생성 $nextFifo = StockLot::withoutGlobalScopes() ->where('stock_id', $stock->id) ->max('fifo_order') ?? 0; StockLot::withoutGlobalScopes()->create([ 'tenant_id' => $tenantId, 'stock_id' => $stock->id, 'lot_no' => $lotNo, 'fifo_order' => $nextFifo + 1, 'receipt_date' => $date, 'qty' => $qty, 'reserved_qty' => 0, 'available_qty' => $qty, 'unit' => $item->unit ?: 'EA', 'supplier' => $supplierName, 'supplier_lot' => $receiving->supplier_lot, 'po_number' => $orderNo, 'location' => $location, 'status' => 'available', 'receiving_id' => $receiving->id, 'created_by' => self::USER_ID, ]); $created['stock_lot']++; // Step 5: IQC 수입검사 생성 $inspectionNo = 'IQC-'.$dateShort.'-'.str_pad($inspectionSeq++, 4, '0', STR_PAD_LEFT); Inspection::withoutGlobalScopes()->create([ 'tenant_id' => $tenantId, 'inspection_no' => $inspectionNo, 'inspection_type' => 'IQC', 'status' => 'completed', 'result' => 'pass', 'request_date' => $date, 'inspection_date' => $date, 'item_id' => $item->id, 'lot_no' => $lotNo, 'meta' => [ 'quantity' => $qty, 'unit' => $item->unit ?: 'EA', 'supplier_name' => $supplierName, 'manufacturer_name' => $manufacturer, 'item_code' => $item->code, 'item_name' => $item->name, ], 'items' => $this->buildInspectionItems($item->name), 'extra' => [ 'remarks' => '입고 시 수입검사 합격', 'opinion' => '양호', ], 'created_by' => self::USER_ID, 'updated_by' => self::USER_ID, ]); $created['inspection']++; $this->line(" [{$item->code}] {$item->name} → 수량:{$qty} | {$receivingNumber} | {$lotNo} | {$supplierName}"); } }); $this->newLine(); $this->info('=== 생성 완료 ==='); $this->table( ['항목', '건수'], [ ['Receiving (입고)', $created['receiving']], ['Stock (재고, 신규)', $created['stock']], ['StockLot (재고LOT)', $created['stock_lot']], ['Inspection (수입검사)', $created['inspection']], ] ); return self::SUCCESS; } /** * 품목명에 따른 공급업체/제조사 반환 * * @return array [업체명, 제조사] */ private function getSupplierForItem(string $itemName): array { $suppliers = self::STEEL_SUPPLIERS; // SUS 품목 → 현대제철 우선 if (str_contains($itemName, 'SUS')) { $pool = [ ['현대제철(주)', '현대제철'], ['동국제강(주)', '동국제강'], ['(주)세아제강', '세아제강'], ]; return $pool[array_rand($pool)]; } // EGI 품목 → 포스코 우선 if (str_contains($itemName, 'EGI')) { $pool = [ ['(주)포스코', '포스코'], ['한국철강(주)', '한국철강'], ['동국제강(주)', '동국제강'], ]; return $pool[array_rand($pool)]; } // 원단류 $fabricPool = array_map( fn ($name, $mfr) => [$name, $mfr], array_keys(self::FABRIC_SUPPLIERS), array_values(self::FABRIC_SUPPLIERS) ); return $fabricPool[array_rand($fabricPool)]; } /** * 품목에 맞는 검사항목 생성 */ private function buildInspectionItems(string $itemName): array { $items = [ [ 'name' => '외관검사', 'type' => 'visual', 'spec' => '표면 흠집, 녹, 이물질 없음', 'unit' => '-', 'result' => '합격', 'measured_value' => '양호', 'judgment' => 'pass', ], ]; // 철판류 (SUS/EGI) 전용 검사항목 if (str_contains($itemName, 'SUS') || str_contains($itemName, 'EGI')) { // 두께 추출 $thickness = $this->extractThickness($itemName); $items[] = [ 'name' => '두께검사', 'type' => 'measurement', 'spec' => $thickness ? "{$thickness}mm ±0.05" : '규격 ±0.05mm', 'unit' => 'mm', 'result' => '적합', 'measured_value' => $thickness ?: '-', 'judgment' => 'pass', ]; $items[] = [ 'name' => '치수검사', 'type' => 'measurement', 'spec' => '가로/세로 규격 ±1mm', 'unit' => 'mm', 'result' => '적합', 'measured_value' => '규격 이내', 'judgment' => 'pass', ]; $items[] = [ 'name' => '재질확인', 'type' => 'certificate', 'spec' => str_contains($itemName, 'SUS') ? 'STS304' : 'SECC', 'unit' => '-', 'result' => '적합', 'measured_value' => str_contains($itemName, 'SUS') ? 'STS304' : 'SECC', 'judgment' => 'pass', ]; } else { // 원단류 기본 검사 $items[] = [ 'name' => '규격검사', 'type' => 'measurement', 'spec' => '제품규격 확인', 'unit' => '-', 'result' => '적합', 'measured_value' => '규격 이내', 'judgment' => 'pass', ]; } return $items; } /** * 품목명에서 두께(mm) 추출 */ private function extractThickness(string $itemName): ?string { // "EGI1.2*..." → 1.2, "SUS1.5*..." → 1.5, "EGI1.6T" → 1.6 if (preg_match('/(SUS|EGI)\s*(\d+\.?\d*)/i', $itemName, $m)) { return $m[2]; } return null; } /** * 다음 입고번호 시퀀스 조회 */ private function getNextReceivingSeq(int $tenantId, string $datePrefix): int { $prefix = 'RV'.$datePrefix; $last = Receiving::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('receiving_number', 'like', $prefix.'%') ->orderBy('receiving_number', 'desc') ->first(['receiving_number']); if ($last) { return ((int) substr($last->receiving_number, -4)) + 1; } return 1; } /** * 다음 LOT번호 시퀀스 조회 */ private function getNextLotSeq(int $tenantId, string $dateShort): int { $last = Receiving::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('lot_no', 'like', $dateShort.'-%') ->orderBy('lot_no', 'desc') ->first(['lot_no']); if ($last) { return ((int) substr($last->lot_no, -2)) + 1; } return 1; } /** * 다음 수입검사번호 시퀀스 조회 */ private function getNextInspectionSeq(int $tenantId, string $dateShort): int { $prefix = 'IQC-'.$dateShort.'-'; $last = Inspection::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('inspection_no', 'like', $prefix.'%') ->orderBy('inspection_no', 'desc') ->first(['inspection_no']); if ($last) { return ((int) substr($last->inspection_no, -4)) + 1; } return 1; } }