diff --git a/app/Models/Tenants/ShipmentItem.php b/app/Models/Tenants/ShipmentItem.php index 88d7eb8..7a790f8 100644 --- a/app/Models/Tenants/ShipmentItem.php +++ b/app/Models/Tenants/ShipmentItem.php @@ -25,6 +25,8 @@ class ShipmentItem extends Model 'unit', 'lot_no', 'stock_lot_id', + 'order_item_id', + 'work_order_item_id', 'remarks', ]; @@ -34,6 +36,8 @@ class ShipmentItem extends Model 'quantity' => 'decimal:2', 'shipment_id' => 'integer', 'stock_lot_id' => 'integer', + 'order_item_id' => 'integer', + 'work_order_item_id' => 'integer', ]; /** @@ -52,6 +56,22 @@ public function stockLot(): BelongsTo return $this->belongsTo(StockLot::class); } + /** + * 수주 품목 관계 + */ + public function orderItem(): BelongsTo + { + return $this->belongsTo(\App\Models\Orders\OrderItem::class); + } + + /** + * 작업지시 품목 관계 + */ + public function workOrderItem(): BelongsTo + { + return $this->belongsTo(\App\Models\WorkOrders\WorkOrderItem::class); + } + /** * 다음 순번 가져오기 */ diff --git a/app/Services/ShipmentService.php b/app/Services/ShipmentService.php index 3db0bec..89cd419 100644 --- a/app/Services/ShipmentService.php +++ b/app/Services/ShipmentService.php @@ -309,6 +309,13 @@ public function updateStatus(int $id, string $status, ?array $additionalData = n $shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id); + // 출하 가능 여부 검증 (scheduled → ready 이상 전환 시) + if (in_array($status, ['ready', 'shipping', 'completed']) && ! $shipment->can_ship) { + throw new \Symfony\Component\HttpKernel\Exception\BadRequestHttpException( + __('error.shipment.cannot_ship') + ); + } + $updateData = [ 'status' => $status, 'updated_by' => $userId, @@ -344,10 +351,8 @@ public function updateStatus(int $id, string $status, ?array $additionalData = n $previousStatus = $shipment->status; $shipment->update($updateData); - // 🆕 출하완료 시 재고 차감 (FIFO) - if ($status === 'completed' && $previousStatus !== 'completed') { - $this->decreaseStockForShipment($shipment); - } + // 재고 차감 비활성화: 수주생산은 재고 미경유, 선생산 완성품은 자재 투입 시 차감됨 + // TODO: 선생산 로직 검증 후 재검토 (decreaseStockForShipment) // 연결된 수주(Order) 상태 동기화 $this->syncOrderStatus($shipment, $tenantId); @@ -357,10 +362,21 @@ public function updateStatus(int $id, string $status, ?array $additionalData = n /** * 출하 완료 시 재고 차감 + * + * 수주 연결 출하(order_id 있음)는 재고를 거치지 않으므로 차감 skip. + * 재고 출고(order_id 없음)만 재고 차감 수행. + * + * @return array 실패 내역 (빈 배열이면 전체 성공) */ - private function decreaseStockForShipment(Shipment $shipment): void + private function decreaseStockForShipment(Shipment $shipment): array { + // 수주 연결 출하는 재고 입고 없이 바로 출하하므로 차감하지 않음 + if ($shipment->order_id) { + return []; + } + $stockService = app(StockService::class); + $failures = []; // 출하 품목 조회 $items = $shipment->items; @@ -389,15 +405,23 @@ private function decreaseStockForShipment(Shipment $shipment): void stockLotId: $item->stock_lot_id ); } catch (\Exception $e) { - // 재고 부족 등의 에러는 로그만 기록하고 계속 진행 \Illuminate\Support\Facades\Log::warning('Failed to decrease stock for shipment item', [ 'shipment_id' => $shipment->id, 'item_code' => $item->item_code, 'quantity' => $item->quantity, 'error' => $e->getMessage(), ]); + + $failures[] = [ + 'item_code' => $item->item_code, + 'item_name' => $item->item_name, + 'quantity' => $item->quantity, + 'reason' => $e->getMessage(), + ]; } } + + return $failures; } /** diff --git a/app/Services/WorkOrderService.php b/app/Services/WorkOrderService.php index 366e419..692048d 100644 --- a/app/Services/WorkOrderService.php +++ b/app/Services/WorkOrderService.php @@ -764,6 +764,8 @@ private function createShipmentFromOrder(Order $order, $mainWorkOrders, int $ten 'quantity' => $result['good_qty'] ?? $woItem->quantity, 'unit' => $woItem->unit, 'lot_no' => $lotNo, + 'order_item_id' => $woItem->source_order_item_id, + 'work_order_item_id' => $woItem->id, 'remarks' => null, ]); } @@ -784,6 +786,8 @@ private function createShipmentFromOrder(Order $order, $mainWorkOrders, int $ten 'quantity' => $orderItem->quantity, 'unit' => $orderItem->unit, 'lot_no' => null, + 'order_item_id' => $orderItem->id, + 'work_order_item_id' => null, 'remarks' => null, ]); } diff --git a/database/migrations/2026_03_13_100000_add_order_item_id_to_shipment_items.php b/database/migrations/2026_03_13_100000_add_order_item_id_to_shipment_items.php new file mode 100644 index 0000000..8632cb8 --- /dev/null +++ b/database/migrations/2026_03_13_100000_add_order_item_id_to_shipment_items.php @@ -0,0 +1,30 @@ +unsignedBigInteger('order_item_id')->nullable()->after('stock_lot_id') + ->comment('수주품목 ID (출처 추적용)'); + $table->unsignedBigInteger('work_order_item_id')->nullable()->after('order_item_id') + ->comment('작업지시품목 ID (출처 추적용)'); + + $table->index('order_item_id'); + $table->index('work_order_item_id'); + }); + } + + public function down(): void + { + Schema::table('shipment_items', function (Blueprint $table) { + $table->dropIndex(['work_order_item_id']); + $table->dropIndex(['order_item_id']); + $table->dropColumn(['work_order_item_id', 'order_item_id']); + }); + } +};