diff --git a/app/Services/WorkOrderService.php b/app/Services/WorkOrderService.php index 62d44aa..d0ee318 100644 --- a/app/Services/WorkOrderService.php +++ b/app/Services/WorkOrderService.php @@ -1092,7 +1092,7 @@ public function updateItemStatus(int $workOrderId, int $itemId, string $status) * 작업지시에 필요한 자재 목록 조회 (BOM 기반 + 로트별 재고) * * 작업지시 품목의 BOM 자재별로 StockLot(입고 로트)를 FIFO 순서로 반환합니다. - * 로트번호는 입고관리(Receiving)에서 생성된 실제 로트번호입니다. + * 동일 자재가 여러 작업지시 품목에 걸쳐 있으면 필요수량을 합산하고 로트는 중복 없이 반환합니다. * * @param int $workOrderId 작업지시 ID * @return array 자재 목록 (로트 단위) @@ -1109,8 +1109,8 @@ public function getMaterials(int $workOrderId): array throw new NotFoundHttpException(__('error.not_found')); } - $materials = []; - $rank = 1; + // Phase 1: 작업지시 품목들에서 유니크 자재 목록 수집 (item_id 기준 합산) + $uniqueMaterials = []; foreach ($workOrder->items as $woItem) { $materialItems = []; @@ -1140,7 +1140,6 @@ public function getMaterials(int $workOrderId): array 'item' => $childItem, 'bom_qty' => $bomQty, 'required_qty' => $bomQty * ($woItem->quantity ?? 1), - 'work_order_item_id' => $woItem->id, ]; } } @@ -1152,73 +1151,83 @@ public function getMaterials(int $workOrderId): array 'item' => $woItem->item, 'bom_qty' => 1, 'required_qty' => $woItem->quantity ?? 1, - 'work_order_item_id' => $woItem->id, ]; } - // 각 자재별로 StockLot(입고 로트) 조회 + // 유니크 자재 수집 (같은 item_id면 required_qty 합산) foreach ($materialItems as $matInfo) { - $materialItem = $matInfo['item']; - - // Stock 조회 - $stock = \App\Models\Tenants\Stock::where('tenant_id', $tenantId) - ->where('item_id', $materialItem->id) - ->first(); - - if ($stock) { - // 가용 로트를 FIFO 순서로 조회 - $lots = \App\Models\Tenants\StockLot::where('tenant_id', $tenantId) - ->where('stock_id', $stock->id) - ->where('status', 'available') - ->where('available_qty', '>', 0) - ->orderBy('fifo_order', 'asc') - ->get(); - - foreach ($lots as $lot) { - $materials[] = [ - 'stock_lot_id' => $lot->id, - 'item_id' => $materialItem->id, - 'work_order_item_id' => $matInfo['work_order_item_id'], - 'lot_no' => $lot->lot_no, - 'material_code' => $materialItem->code, - 'material_name' => $materialItem->name, - 'specification' => $materialItem->specification, - 'unit' => $lot->unit ?? $materialItem->unit ?? 'EA', - 'bom_qty' => $matInfo['bom_qty'], - 'required_qty' => $matInfo['required_qty'], - 'lot_qty' => (float) $lot->qty, - 'lot_available_qty' => (float) $lot->available_qty, - 'lot_reserved_qty' => (float) $lot->reserved_qty, - 'receipt_date' => $lot->receipt_date, - 'supplier' => $lot->supplier, - 'fifo_rank' => $rank++, - ]; - } + $itemId = $matInfo['item']->id; + if (isset($uniqueMaterials[$itemId])) { + $uniqueMaterials[$itemId]['required_qty'] += $matInfo['required_qty']; + } else { + $uniqueMaterials[$itemId] = $matInfo; } + } + } - // 가용 로트가 없는 경우 자재 정보만 반환 (재고 없음 표시) - $hasLots = collect($materials)->where('item_id', $materialItem->id)->isNotEmpty(); - if (! $hasLots) { + // Phase 2: 유니크 자재별로 StockLot 조회 + $materials = []; + $rank = 1; + + foreach ($uniqueMaterials as $matInfo) { + $materialItem = $matInfo['item']; + + $stock = \App\Models\Tenants\Stock::where('tenant_id', $tenantId) + ->where('item_id', $materialItem->id) + ->first(); + + $lotsFound = false; + + if ($stock) { + $lots = \App\Models\Tenants\StockLot::where('tenant_id', $tenantId) + ->where('stock_id', $stock->id) + ->where('status', 'available') + ->where('available_qty', '>', 0) + ->orderBy('fifo_order', 'asc') + ->get(); + + foreach ($lots as $lot) { + $lotsFound = true; $materials[] = [ - 'stock_lot_id' => null, + 'stock_lot_id' => $lot->id, 'item_id' => $materialItem->id, - 'work_order_item_id' => $matInfo['work_order_item_id'], - 'lot_no' => null, + 'lot_no' => $lot->lot_no, 'material_code' => $materialItem->code, 'material_name' => $materialItem->name, 'specification' => $materialItem->specification, - 'unit' => $materialItem->unit ?? 'EA', + 'unit' => $lot->unit ?? $materialItem->unit ?? 'EA', 'bom_qty' => $matInfo['bom_qty'], 'required_qty' => $matInfo['required_qty'], - 'lot_qty' => 0, - 'lot_available_qty' => 0, - 'lot_reserved_qty' => 0, - 'receipt_date' => null, - 'supplier' => null, + 'lot_qty' => (float) $lot->qty, + 'lot_available_qty' => (float) $lot->available_qty, + 'lot_reserved_qty' => (float) $lot->reserved_qty, + 'receipt_date' => $lot->receipt_date, + 'supplier' => $lot->supplier, 'fifo_rank' => $rank++, ]; } } + + // 가용 로트가 없는 경우 자재 정보만 반환 (재고 없음 표시) + if (! $lotsFound) { + $materials[] = [ + 'stock_lot_id' => null, + 'item_id' => $materialItem->id, + 'lot_no' => null, + 'material_code' => $materialItem->code, + 'material_name' => $materialItem->name, + 'specification' => $materialItem->specification, + 'unit' => $materialItem->unit ?? 'EA', + 'bom_qty' => $matInfo['bom_qty'], + 'required_qty' => $matInfo['required_qty'], + 'lot_qty' => 0, + 'lot_available_qty' => 0, + 'lot_reserved_qty' => 0, + 'receipt_date' => null, + 'supplier' => null, + 'fifo_rank' => $rank++, + ]; + } } return $materials;