From 5ee97c2d74e99ce5c5edc0c2936fa9e77f22db65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 4 Mar 2026 10:36:38 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20[production]=20=EC=9E=90=EC=9E=AC?= =?UTF-8?q?=ED=88=AC=EC=9E=85=20bom=5Fgroup=5Fkey=20=EA=B0=9C=EB=B3=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=E2=80=94=20=EB=8F=99=EC=9D=BC=20=EC=9E=90?= =?UTF-8?q?=EC=9E=AC=20=EB=8B=A4=EC=A4=91=20BOM=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bom_group_key 컬럼 추가 마이그레이션 (work_order_material_inputs) - WorkOrderMaterialInput 모델 fillable에 bom_group_key 추가 - MaterialInputForItemRequest에 bom_group_key 검증 + replace 옵션 추가 - WorkOrderService.getMaterialsForItem: stock_lot_id+bom_group_key 복합키 기투입 조회 (하위호환) - WorkOrderService.registerMaterialInputForItem: bom_group_key 저장 + replace 모드 (기존 삭제→재등록) Co-Authored-By: Claude Opus 4.6 --- .../WorkOrder/MaterialInputForItemRequest.php | 2 + .../Production/WorkOrderMaterialInput.php | 1 + app/Services/WorkOrderService.php | 52 ++++++++++++++++++- ...roup_key_to_work_order_material_inputs.php | 26 ++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2026_03_04_100000_add_bom_group_key_to_work_order_material_inputs.php diff --git a/app/Http/Requests/WorkOrder/MaterialInputForItemRequest.php b/app/Http/Requests/WorkOrder/MaterialInputForItemRequest.php index d1c7a5f..1af761e 100644 --- a/app/Http/Requests/WorkOrder/MaterialInputForItemRequest.php +++ b/app/Http/Requests/WorkOrder/MaterialInputForItemRequest.php @@ -17,6 +17,8 @@ public function rules(): array 'inputs' => 'required|array|min:1', 'inputs.*.stock_lot_id' => 'required|integer', 'inputs.*.qty' => 'required|numeric|gt:0', + 'inputs.*.bom_group_key' => 'sometimes|nullable|string|max:100', + 'replace' => 'sometimes|boolean', ]; } diff --git a/app/Models/Production/WorkOrderMaterialInput.php b/app/Models/Production/WorkOrderMaterialInput.php index c921d34..92fe21f 100644 --- a/app/Models/Production/WorkOrderMaterialInput.php +++ b/app/Models/Production/WorkOrderMaterialInput.php @@ -27,6 +27,7 @@ class WorkOrderMaterialInput extends Model 'work_order_item_id', 'stock_lot_id', 'item_id', + 'bom_group_key', 'qty', 'input_by', 'input_at', diff --git a/app/Services/WorkOrderService.php b/app/Services/WorkOrderService.php index bb5c5f9..e5d8601 100644 --- a/app/Services/WorkOrderService.php +++ b/app/Services/WorkOrderService.php @@ -3240,6 +3240,30 @@ public function getMaterialsForItem(int $workOrderId, int $itemId): array ->groupBy('item_id') ->pluck('total_qty', 'item_id'); + // LOT별 기투입 수량 조회 (stock_lot_id + bom_group_key별 SUM) + $lotInputtedRaw = WorkOrderMaterialInput::where('tenant_id', $tenantId) + ->where('work_order_id', $workOrderId) + ->where('work_order_item_id', $itemId) + ->whereNotNull('stock_lot_id') + ->selectRaw('stock_lot_id, bom_group_key, SUM(qty) as total_qty') + ->groupBy('stock_lot_id', 'bom_group_key') + ->get(); + + // bom_group_key 포함 복합키 매핑 + stock_lot_id 단순 매핑 (하위호환) + $lotInputtedByGroup = []; + $lotInputtedByLot = []; + foreach ($lotInputtedRaw as $row) { + $lotId = $row->stock_lot_id; + $groupKey = $row->bom_group_key; + $qty = (float) $row->total_qty; + + if ($groupKey) { + $compositeKey = $lotId.'_'.$groupKey; + $lotInputtedByGroup[$compositeKey] = ($lotInputtedByGroup[$compositeKey] ?? 0) + $qty; + } + $lotInputtedByLot[$lotId] = ($lotInputtedByLot[$lotId] ?? 0) + $qty; + } + // 자재별 LOT 조회 $materials = []; $rank = 1; @@ -3283,6 +3307,7 @@ public function getMaterialsForItem(int $workOrderId, int $itemId): array 'required_qty' => $matInfo['required_qty'], 'already_inputted' => $alreadyInputted, 'remaining_required_qty' => $remainingRequired, + 'lot_inputted_qty' => (float) ($lotInputtedByGroup[$lot->id.'_'.$bomGroupKey] ?? $lotInputtedByLot[$lot->id] ?? 0), 'lot_qty' => (float) $lot->qty, 'lot_available_qty' => (float) $lot->available_qty, 'lot_reserved_qty' => (float) $lot->reserved_qty, @@ -3310,6 +3335,7 @@ public function getMaterialsForItem(int $workOrderId, int $itemId): array 'required_qty' => $matInfo['required_qty'], 'already_inputted' => $alreadyInputted, 'remaining_required_qty' => $remainingRequired, + 'lot_inputted_qty' => 0, 'lot_qty' => 0, 'lot_available_qty' => 0, 'lot_reserved_qty' => 0, @@ -3328,8 +3354,10 @@ public function getMaterialsForItem(int $workOrderId, int $itemId): array /** * 개소별 자재 투입 등록 + * + * @param bool $replace true면 기존 투입 이력을 삭제(재고 복원) 후 새로 등록 */ - public function registerMaterialInputForItem(int $workOrderId, int $itemId, array $inputs): array + public function registerMaterialInputForItem(int $workOrderId, int $itemId, array $inputs, bool $replace = false): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); @@ -3347,13 +3375,32 @@ public function registerMaterialInputForItem(int $workOrderId, int $itemId, arra throw new NotFoundHttpException(__('error.not_found')); } - return DB::transaction(function () use ($inputs, $tenantId, $userId, $workOrderId, $itemId) { + return DB::transaction(function () use ($inputs, $tenantId, $userId, $workOrderId, $itemId, $replace) { $stockService = app(StockService::class); $inputResults = []; + // replace 모드: 기존 투입 이력 삭제 + 재고 복원 + if ($replace) { + $existingInputs = WorkOrderMaterialInput::where('tenant_id', $tenantId) + ->where('work_order_id', $workOrderId) + ->where('work_order_item_id', $itemId) + ->get(); + + foreach ($existingInputs as $existing) { + $stockService->increaseToLot( + stockLotId: $existing->stock_lot_id, + qty: (float) $existing->qty, + reason: 'work_order_input_replace', + referenceId: $workOrderId + ); + $existing->delete(); + } + } + foreach ($inputs as $input) { $stockLotId = $input['stock_lot_id'] ?? null; $qty = (float) ($input['qty'] ?? 0); + $bomGroupKey = $input['bom_group_key'] ?? null; if (! $stockLotId || $qty <= 0) { continue; @@ -3378,6 +3425,7 @@ public function registerMaterialInputForItem(int $workOrderId, int $itemId, arra 'work_order_item_id' => $itemId, 'stock_lot_id' => $stockLotId, 'item_id' => $lotItemId ?? 0, + 'bom_group_key' => $bomGroupKey, 'qty' => $qty, 'input_by' => $userId, 'input_at' => now(), diff --git a/database/migrations/2026_03_04_100000_add_bom_group_key_to_work_order_material_inputs.php b/database/migrations/2026_03_04_100000_add_bom_group_key_to_work_order_material_inputs.php new file mode 100644 index 0000000..8c451c1 --- /dev/null +++ b/database/migrations/2026_03_04_100000_add_bom_group_key_to_work_order_material_inputs.php @@ -0,0 +1,26 @@ +string('bom_group_key')->nullable()->after('item_id') + ->comment('BOM 그룹키 (같은 item_id의 다른 용도 구분, ex: itemId_category_partType)'); + + $table->index(['work_order_item_id', 'bom_group_key'], 'idx_womi_item_bomgroup'); + }); + } + + public function down(): void + { + Schema::table('work_order_material_inputs', function (Blueprint $table) { + $table->dropIndex('idx_womi_item_bomgroup'); + $table->dropColumn('bom_group_key'); + }); + } +};