load('unitOfMeasure'); // Solo validar seriales si track_serials Y la unidad NO permite decimales $requiresSerials = $inventory->track_serials && !$inventory->unitOfMeasure?->allows_decimals; if ($requiresSerials) { if (empty($serialNumbers)) { throw new \Exception('Este producto requiere números de serie. Debe proporcionar ' . $quantity . ' seriales.'); } // Filtrar seriales vacíos y hacer trim $serialNumbers = array_filter(array_map('trim', $serialNumbers), function($serial) { return !empty($serial); }); // Re-indexar el array $serialNumbers = array_values($serialNumbers); $serialCount = count($serialNumbers); if ($serialCount != $quantity) { throw new \Exception("La cantidad de seriales ({$serialCount}) no coincide con la cantidad ({$quantity}). Seriales recibidos: " . implode(', ', $serialNumbers)); } } // Obtener stock actual para calcular costo promedio ponderado $currentStock = $inventory->stock; $currentCost = $inventory->price?->cost ?? 0.00; // Calcular nuevo costo promedio ponderado $newCost = $this->calculateWeightedAverageCost( $currentStock, $currentCost, $quantity, $unitCost ); // Actualizar costo en prices $this->updateProductCost($inventory, $newCost); // Solo crear seriales si se proporcionaron if ($requiresSerials && !empty($serialNumbers)) { foreach ($serialNumbers as $serialNumber) { InventorySerial::create([ 'inventory_id' => $inventory->id, 'warehouse_id' => $warehouse->id, 'serial_number' => trim($serialNumber), 'status' => 'disponible', ]); } // Activar track_serials si no estaba activo if (!$inventory->track_serials) { $inventory->update(['track_serials' => true]); } // Sincronizar stock desde seriales $inventory->syncStock(); } else { // **SIN SERIALES**: Actualizar stock manualmente (permite decimales) $this->updateWarehouseStock($inventory->id, $warehouse->id, $quantity); } // Registrar movimiento return InventoryMovement::create([ 'inventory_id' => $inventory->id, 'warehouse_from_id' => null, 'warehouse_to_id' => $warehouse->id, 'movement_type' => 'entry', 'quantity' => $quantity, 'unit_cost' => $unitCost, 'user_id' => auth()->id(), 'notes' => $data['notes'] ?? null, 'invoice_reference' => $data['invoice_reference'] ?? null, ]); }); } /** * Entrada múltiple de inventario (varios productos) */ public function bulkEntry(array $data): array { return DB::transaction(function () use ($data) { $warehouse = Warehouse::findOrFail($data['warehouse_id']); $movements = []; foreach ($data['products'] as $productData) { $inventory = Inventory::findOrFail($productData['inventory_id']); $quantity = (float) $productData['quantity']; $unitCost = (float)$productData['unit_cost']; $serialNumbers = $productData['serial_numbers'] ?? null; // Validar seriales si el producto los requiere if ($inventory->track_serials) { if (empty($serialNumbers)) { throw new \Exception("El producto '{$inventory->name}' requiere números de serie. Debe proporcionar {$quantity} seriales."); } // Filtrar seriales vacíos y hacer trim $serialNumbers = array_filter(array_map('trim', $serialNumbers), function($serial) { return !empty($serial); }); // Re-indexar el array $serialNumbers = array_values($serialNumbers); $serialCount = count($serialNumbers); if ($serialCount != $quantity) { throw new \Exception("El producto '{$inventory->name}': cantidad de seriales ({$serialCount}) no coincide con cantidad ({$quantity}). Seriales recibidos: " . implode(', ', $serialNumbers)); } // Actualizar el array limpio $productData['serial_numbers'] = $serialNumbers; } // Obtener stock actual para calcular costo promedio ponderado $currentStock = $inventory->stock; $currentCost = $inventory->price?->cost ?? 0.00; // Calcular nuevo costo promedio ponderado $newCost = $this->calculateWeightedAverageCost( $currentStock, $currentCost, $quantity, $unitCost ); // Actualizar costo en prices $this->updateProductCost($inventory, $newCost); // Crear seriales si se proporcionan if (!empty($serialNumbers)) { foreach ($serialNumbers as $serialNumber) { InventorySerial::create([ 'inventory_id' => $inventory->id, 'warehouse_id' => $warehouse->id, 'serial_number' => trim($serialNumber), 'status' => 'disponible', ]); } // Activar track_serials si no estaba activo if (!$inventory->track_serials) { $inventory->update(['track_serials' => true]); } // Sincronizar stock desde seriales $inventory->syncStock(); } else { // Sin seriales, actualizar stock manualmente $this->updateWarehouseStock($inventory->id, $warehouse->id, $quantity); } // Registrar movimiento $movement = InventoryMovement::create([ 'inventory_id' => $inventory->id, 'warehouse_from_id' => null, 'warehouse_to_id' => $warehouse->id, 'movement_type' => 'entry', 'quantity' => $quantity, 'unit_cost' => $unitCost, 'user_id' => auth()->id(), 'notes' => $data['notes'] ?? null, 'invoice_reference' => $data['invoice_reference'], ]); $movements[] = $movement->load(['inventory', 'warehouseTo']); } return $movements; }); } /** * Salida múltiple de inventario (varios productos) */ public function bulkExit(array $data): array { return DB::transaction(function () use ($data) { $warehouse = Warehouse::findOrFail($data['warehouse_id']); $movements = []; foreach ($data['products'] as $productData) { $inventory = Inventory::with('unitOfMeasure')->findOrFail($productData['inventory_id']); $quantity = (float) $productData['quantity']; $serialNumbers = $productData['serial_numbers'] ?? null; // **NUEVO**: Solo exigir seriales si track_serials Y unidad NO permite decimales $requiresSerials = $inventory->track_serials && !$inventory->unitOfMeasure?->allows_decimals; // Validar y procesar seriales si el producto los requiere if ($requiresSerials) { if (empty($serialNumbers)) { throw new \Exception("El producto '{$inventory->name}' requiere números de serie. Debe proporcionar {$quantity} seriales."); } // Filtrar seriales vacíos y hacer trim $serialNumbers = array_filter(array_map('trim', $serialNumbers), function($serial) { return !empty($serial); }); $serialNumbers = array_values($serialNumbers); $serialCount = count($serialNumbers); if ($serialCount !== $quantity) { throw new \Exception("El producto '{$inventory->name}': cantidad de seriales ({$serialCount}) no coincide con cantidad ({$quantity}). Ajuste la cantidad o agregue/elimine seriales."); } // Validar que los seriales existan, pertenezcan al producto, estén disponibles y en el almacén correcto foreach ($serialNumbers as $serialNumber) { $serial = InventorySerial::where('serial_number', $serialNumber) ->where('inventory_id', $inventory->id) ->first(); if (!$serial) { throw new \Exception("Producto '{$inventory->name}': El serial '{$serialNumber}' no pertenece a este producto."); } if ($serial->status !== 'disponible') { throw new \Exception("Producto '{$inventory->name}': El serial '{$serialNumber}' no está disponible (estado: {$serial->status})."); } if ($serial->warehouse_id !== $warehouse->id) { throw new \Exception("Producto '{$inventory->name}': El serial '{$serialNumber}' no está en el almacén seleccionado."); } } // Eliminar los seriales (salida definitiva) InventorySerial::whereIn('serial_number', $serialNumbers) ->where('inventory_id', $inventory->id) ->delete(); // Sincronizar stock desde seriales $inventory->syncStock(); } else { // Sin seriales, validar y decrementar stock manualmente $this->validateStock($inventory->id, $warehouse->id, $quantity); $this->updateWarehouseStock($inventory->id, $warehouse->id, -$quantity); } // Registrar movimiento $movement = InventoryMovement::create([ 'inventory_id' => $inventory->id, 'warehouse_from_id' => $warehouse->id, 'warehouse_to_id' => null, 'movement_type' => 'exit', 'quantity' => $quantity, 'user_id' => auth()->id(), 'notes' => $data['notes'] ?? null, ]); $movements[] = $movement->load(['inventory', 'warehouseFrom']); } return $movements; }); } /** * Traspaso múltiple entre almacenes (varios productos) */ public function bulkTransfer(array $data): array { return DB::transaction(function () use ($data) { $warehouseFrom = Warehouse::findOrFail($data['warehouse_from_id']); $warehouseTo = Warehouse::findOrFail($data['warehouse_to_id']); $movements = []; // Validar que no sea el mismo almacén if ($warehouseFrom->id === $warehouseTo->id) { throw new \Exception('No se puede traspasar al mismo almacén.'); } foreach ($data['products'] as $productData) { $inventory = Inventory::findOrFail($productData['inventory_id']); $quantity = (float) $productData['quantity']; $serialNumbers = (array) ($productData['serial_numbers'] ?? null); // Validar y procesar seriales si el producto los requiere if ($inventory->track_serials) { if (empty($serialNumbers)) { throw new \Exception("El producto '{$inventory->name}' requiere números de serie. Debe proporcionar {$quantity} seriales."); } // Filtrar seriales vacíos y hacer trim $serialNumbers = array_filter(array_map('trim', $serialNumbers), function($serial) { return !empty($serial); }); $serialNumbers = array_values($serialNumbers); $serialCount = count($serialNumbers); if ($serialCount !== $quantity) { throw new \Exception("El producto '{$inventory->name}': cantidad de seriales ({$serialCount}) no coincide con cantidad ({$quantity}). Ajuste la cantidad o agregue/elimine seriales."); } // Validar que los seriales existan, pertenezcan al producto, estén disponibles y en el almacén origen foreach ($serialNumbers as $serialNumber) { $serial = InventorySerial::where('serial_number', $serialNumber) ->where('inventory_id', $inventory->id) ->first(); if (!$serial) { throw new \Exception("Producto '{$inventory->name}': El serial '{$serialNumber}' no pertenece a este producto."); } if ($serial->status !== 'disponible') { throw new \Exception("Producto '{$inventory->name}': El serial '{$serialNumber}' no está disponible (estado: {$serial->status})."); } if ($serial->warehouse_id !== $warehouseFrom->id) { throw new \Exception("Producto '{$inventory->name}': El serial '{$serialNumber}' no está en el almacén origen."); } } // Actualizar warehouse_id de los seriales seleccionados InventorySerial::whereIn('serial_number', $serialNumbers) ->where('inventory_id', $inventory->id) ->update(['warehouse_id' => $warehouseTo->id]); // Sincronizar stock desde seriales (actualiza ambos almacenes) $inventory->syncStock(); } else { // Sin seriales, validar y actualizar stock manualmente $this->validateStock($inventory->id, $warehouseFrom->id, $quantity); $this->updateWarehouseStock($inventory->id, $warehouseFrom->id, -$quantity); $this->updateWarehouseStock($inventory->id, $warehouseTo->id, $quantity); } // Registrar movimiento $movement = InventoryMovement::create([ 'inventory_id' => $inventory->id, 'warehouse_from_id' => $warehouseFrom->id, 'warehouse_to_id' => $warehouseTo->id, 'movement_type' => 'transfer', 'quantity' => $quantity, 'user_id' => auth()->id(), 'notes' => $data['notes'] ?? null, ]); $movements[] = $movement->load(['inventory', 'warehouseFrom', 'warehouseTo']); } return $movements; }); } /** * Calcular costo promedio ponderado */ protected function calculateWeightedAverageCost( float $currentStock, float $currentCost, float $entryQuantity, float $entryCost ): float { if ($currentStock <= 0) { return $entryCost; } $totalValue = ($currentStock * $currentCost) + ($entryQuantity * $entryCost); $totalQuantity = $currentStock + $entryQuantity; return round($totalValue / $totalQuantity, 2); } protected function updateProductCost(Inventory $inventory, float $newCost): void { $inventory->price()->updateOrCreate( ['inventory_id' => $inventory->id], ['cost' => $newCost] ); } /** * Salida de inventario (merma, ajuste negativo, robo, daño) */ public function exit(array $data): InventoryMovement { return DB::transaction(function () use ($data) { $inventory = Inventory::findOrFail($data['inventory_id']); $warehouse = Warehouse::findOrFail($data['warehouse_id']); $quantity = (float) $data['quantity']; $serialNumbers = $data['serial_numbers'] ?? null; // **NUEVO**: Cargar la unidad de medida $inventory->load('unitOfMeasure'); // **CAMBIO**: Solo validar seriales si track_serials Y la unidad NO permite decimales $requiresSerials = $inventory->track_serials && !$inventory->unitOfMeasure?->allows_decimals; // Validar y procesar seriales si el producto los requiere if ($requiresSerials) { if (empty($serialNumbers)) { throw new \Exception('Este producto requiere números de serie. Debe proporcionar ' . $quantity . ' seriales.'); } // Filtrar seriales vacíos y hacer trim $serialNumbers = array_filter(array_map('trim', $serialNumbers), function($serial) { return !empty($serial); }); $serialNumbers = array_values($serialNumbers); $serialCount = count($serialNumbers); if ($serialCount !== $quantity) { throw new \Exception("La cantidad de seriales ({$serialCount}) no coincide con la cantidad ({$quantity}). Ajuste la cantidad o agregue/elimine seriales."); } // Validar que los seriales existan, pertenezcan al producto, estén disponibles y en el almacén correcto foreach ($serialNumbers as $serialNumber) { $serial = InventorySerial::where('serial_number', $serialNumber) ->where('inventory_id', $inventory->id) ->first(); if (!$serial) { throw new \Exception("El serial '{$serialNumber}' no pertenece a este producto."); } if ($serial->status !== 'disponible') { throw new \Exception("El serial '{$serialNumber}' no está disponible (estado: {$serial->status})."); } if ($serial->warehouse_id !== $warehouse->id) { throw new \Exception("El serial '{$serialNumber}' no está en el almacén seleccionado."); } } // Eliminar los seriales (salida definitiva) InventorySerial::whereIn('serial_numbers', $serialNumbers) ->where('inventory_id', $inventory->id) ->delete(); // Sincronizar stock desde seriales $inventory->syncStock(); } else { // **SIN SERIALES**: Validar y decrementar stock manualmente $this->validateStock($inventory->id, $warehouse->id, $quantity); $this->updateWarehouseStock($inventory->id, $warehouse->id, -$quantity); } // Registrar movimiento return InventoryMovement::create([ 'inventory_id' => $inventory->id, 'warehouse_from_id' => $warehouse->id, 'warehouse_to_id' => null, 'movement_type' => 'exit', 'quantity' => $quantity, 'user_id' => auth()->id(), 'notes' => $data['notes'] ?? null, ]); }); } /** * Traspaso entre almacenes */ public function transfer(array $data): InventoryMovement { return DB::transaction(function () use ($data) { $inventory = Inventory::with('unitOfMeasure')->findOrFail($data['inventory_id']); $warehouseFrom = Warehouse::findOrFail($data['warehouse_from_id']); $warehouseTo = Warehouse::findOrFail($data['warehouse_to_id']); $quantity = (float) $data['quantity']; $serialNumbers = $data['serial_numbers'] ?? null; // Validar que no sea el mismo almacén if ($warehouseFrom->id === $warehouseTo->id) { throw new \Exception('No se puede traspasar al mismo almacén.'); } // **NUEVO**: Solo exigir seriales si track_serials Y unidad NO permite decimales $requiresSerials = $inventory->track_serials && !$inventory->unitOfMeasure?->allows_decimals; // Validar y procesar seriales si el producto los requiere if ($requiresSerials) { if (empty($serialNumbers)) { throw new \Exception('Este producto requiere números de serie. Debe proporcionar ' . $quantity . ' seriales.'); } // Filtrar seriales vacíos y hacer trim $serialNumbers = array_filter(array_map('trim', $serialNumbers), function($serial) { return !empty($serial); }); $serialNumbers = array_values($serialNumbers); $serialCount = count($serialNumbers); if ($serialCount !== $quantity) { throw new \Exception("La cantidad de seriales ({$serialCount}) no coincide con la cantidad ({$quantity}). Ajuste la cantidad o agregue/elimine seriales."); } // Validar que los seriales existan, pertenezcan al producto, estén disponibles y en el almacén origen foreach ($serialNumbers as $serialNumber) { $serial = InventorySerial::where('serial_number', $serialNumber) ->where('inventory_id', $inventory->id) ->first(); if (!$serial) { throw new \Exception("El serial '{$serialNumber}' no pertenece a este producto."); } if ($serial->status !== 'disponible') { throw new \Exception("El serial '{$serialNumber}' no está disponible (estado: {$serial->status})."); } if ($serial->warehouse_id !== $warehouseFrom->id) { throw new \Exception("El serial '{$serialNumber}' no está en el almacén origen."); } } // Actualizar warehouse_id de los seriales seleccionados InventorySerial::whereIn('serial_number', $serialNumbers) ->where('inventory_id', $inventory->id) ->update(['warehouse_id' => $warehouseTo->id]); // Sincronizar stock desde seriales (actualiza ambos almacenes) $inventory->syncStock(); } else { // Sin seriales, validar y actualizar stock manualmente $this->validateStock($inventory->id, $warehouseFrom->id, $quantity); $this->updateWarehouseStock($inventory->id, $warehouseFrom->id, -$quantity); $this->updateWarehouseStock($inventory->id, $warehouseTo->id, $quantity); } // Registrar movimiento return InventoryMovement::create([ 'inventory_id' => $inventory->id, 'warehouse_from_id' => $warehouseFrom->id, 'warehouse_to_id' => $warehouseTo->id, 'movement_type' => 'transfer', 'quantity' => $quantity, 'user_id' => auth()->id(), 'notes' => $data['notes'] ?? null, ]); }); } /** * Actualizar stock en inventory_warehouse */ public function updateWarehouseStock(int $inventoryId, int $warehouseId, float $quantityChange): void { $record = InventoryWarehouse::firstOrCreate( [ 'inventory_id' => $inventoryId, 'warehouse_id' => $warehouseId, ], ['stock' => 0] ); $newStock = $record->stock + $quantityChange; if ($newStock < 0) { throw new \Exception('Stock insuficiente en el almacén.'); } $record->update(['stock' => $newStock]); } /** * Validar stock disponible */ public function validateStock(int $inventoryId, int $warehouseId, float $requiredQuantity): void { $record = InventoryWarehouse::where('inventory_id', $inventoryId) ->where('warehouse_id', $warehouseId) ->first(); $availableStock = $record?->stock ?? 0; if ($availableStock < $requiredQuantity) { throw new \Exception("Stock insuficiente. Disponible: {$availableStock}, Requerido: {$requiredQuantity}"); } } /** * Registrar movimiento de venta */ public function recordSale(int $inventoryId, int $warehouseId, float $quantity, int $saleId): InventoryMovement { return InventoryMovement::create([ 'inventory_id' => $inventoryId, 'warehouse_from_id' => $warehouseId, 'warehouse_to_id' => null, 'movement_type' => 'sale', 'quantity' => $quantity, 'reference_type' => 'App\Models\Sale', 'reference_id' => $saleId, 'user_id' => auth()->id(), ]); } /** * Registrar movimiento de devolución */ public function recordReturn(int $inventoryId, int $warehouseId, float $quantity, int $returnId): InventoryMovement { return InventoryMovement::create([ 'inventory_id' => $inventoryId, 'warehouse_from_id' => null, 'warehouse_to_id' => $warehouseId, 'movement_type' => 'return', 'quantity' => $quantity, 'reference_type' => 'App\Models\Returns', 'reference_id' => $returnId, 'user_id' => auth()->id(), ]); } /** * Obtener el almacén principal */ public function getMainWarehouseId(): int { $warehouse = Warehouse::where('is_main', true)->first(); if (!$warehouse) { throw new \Exception('No existe un almacén principal configurado.'); } return $warehouse->id; } /** * Actualizar un movimiento existente */ public function updateMovement(int $movementId, array $data): InventoryMovement { return DB::transaction(function () use ($movementId, $data) { $movement = InventoryMovement::findOrFail($movementId); // No permitir editar movimientos de venta o devolución (son automáticos) if (in_array($movement->movement_type, ['sale', 'return'])) { throw new \Exception('No se pueden editar movimientos de venta o devolución.'); } // 1. Revertir el movimiento original $this->revertMovement($movement); // 2. Preparar datos completos para actualización según tipo $updateData = $this->prepareUpdateData($movement, $data); // 3. Aplicar el nuevo movimiento según el tipo switch ($movement->movement_type) { case 'entry': $this->applyEntryUpdate($movement, $updateData); break; case 'exit': $this->applyExitUpdate($movement, $updateData); break; case 'transfer': $this->applyTransferUpdate($movement, $updateData); break; } // 4. Actualizar el registro del movimiento $movement->update($updateData); return $movement->load(['inventory', 'warehouseFrom', 'warehouseTo', 'user']); }); } /** * Preparar datos completos para actualización */ protected function prepareUpdateData(InventoryMovement $movement, array $data): array { $updateData = [ 'quantity' => $data['quantity'] ?? $movement->quantity, 'notes' => $data['notes'] ?? $movement->notes, ]; // Campos específicos por tipo de movimiento if ($movement->movement_type === 'entry') { $updateData['unit_cost'] = $data['unit_cost'] ?? $movement->unit_cost; $updateData['invoice_reference'] = $data['invoice_reference'] ?? $movement->invoice_reference; $updateData['warehouse_to_id'] = $data['warehouse_to_id'] ?? $movement->warehouse_to_id; } elseif ($movement->movement_type === 'exit') { $updateData['warehouse_from_id'] = $data['warehouse_from_id'] ?? $movement->warehouse_from_id; } elseif ($movement->movement_type === 'transfer') { $updateData['warehouse_from_id'] = $data['warehouse_from_id'] ?? $movement->warehouse_from_id; $updateData['warehouse_to_id'] = $data['warehouse_to_id'] ?? $movement->warehouse_to_id; } return $updateData; } /** * Revertir el impacto de un movimiento en el stock */ protected function revertMovement(InventoryMovement $movement): void { switch ($movement->movement_type) { case 'entry': // Revertir entrada: restar del almacén destino $this->updateWarehouseStock( $movement->inventory_id, $movement->warehouse_to_id, -$movement->quantity ); break; case 'exit': // Revertir salida: devolver al almacén origen $this->updateWarehouseStock( $movement->inventory_id, $movement->warehouse_from_id, $movement->quantity ); break; case 'transfer': // Revertir traspaso: devolver al origen, quitar del destino $this->updateWarehouseStock( $movement->inventory_id, $movement->warehouse_from_id, $movement->quantity ); $this->updateWarehouseStock( $movement->inventory_id, $movement->warehouse_to_id, -$movement->quantity ); break; } } /** * Aplicar actualización de entrada */ protected function applyEntryUpdate(InventoryMovement $movement, array $data): void { $inventory = $movement->inventory; $warehouseToId = $data['warehouse_to_id'] ?? $movement->warehouse_to_id; $quantity = $data['quantity'] ?? $movement->quantity; $unitCost = $data['unit_cost'] ?? $movement->unit_cost; // Recalcular costo promedio ponderado $currentStock = $inventory->stock; $currentCost = $inventory->price?->cost ?? 0.00; $newCost = $this->calculateWeightedAverageCost( $currentStock, $currentCost, $quantity, $unitCost ); // Actualizar costo $this->updateProductCost($inventory, $newCost); // Aplicar nuevo stock $this->updateWarehouseStock($inventory->id, $warehouseToId, $quantity); } /** * Aplicar actualización de salida */ protected function applyExitUpdate(InventoryMovement $movement, array $data): void { $warehouseFromId = $data['warehouse_from_id'] ?? $movement->warehouse_from_id; $quantity = $data['quantity'] ?? $movement->quantity; // Validar stock disponible $this->validateStock($movement->inventory_id, $warehouseFromId, $quantity); // Aplicar nueva salida $this->updateWarehouseStock($movement->inventory_id, $warehouseFromId, -$quantity); } /** * Aplicar actualización de traspaso */ protected function applyTransferUpdate(InventoryMovement $movement, array $data): void { $warehouseFromId = $data['warehouse_from_id'] ?? $movement->warehouse_from_id; $warehouseToId = $data['warehouse_to_id'] ?? $movement->warehouse_to_id; $quantity = $data['quantity'] ?? $movement->quantity; // Validar que no sea el mismo almacén if ($warehouseFromId === $warehouseToId) { throw new \Exception('No se puede traspasar al mismo almacén.'); } // Validar stock disponible en origen $this->validateStock($movement->inventory_id, $warehouseFromId, $quantity); // Aplicar nuevo traspaso $this->updateWarehouseStock($movement->inventory_id, $warehouseFromId, -$quantity); $this->updateWarehouseStock($movement->inventory_id, $warehouseToId, $quantity); } /** * Sincronizar stock en inventory_warehouse basado en seriales disponibles * Solo aplica para productos con track_serials = true */ public function syncStockFromSerials(Inventory $inventory): void { if (!$inventory->track_serials) { return; } // Contar seriales disponibles por almacén $stockByWarehouse = $inventory->serials() ->where('status', 'disponible') ->whereNotNull('warehouse_id') ->selectRaw('warehouse_id, COUNT(*) as total') ->groupBy('warehouse_id') ->pluck('total', 'warehouse_id'); // Actualizar stock en cada almacén foreach ($stockByWarehouse as $warehouseId => $count) { InventoryWarehouse::updateOrCreate( [ 'inventory_id' => $inventory->id, 'warehouse_id' => $warehouseId, ], ['stock' => $count] ); } // Poner en 0 los almacenes que ya no tienen seriales disponibles InventoryWarehouse::where('inventory_id', $inventory->id) ->whereNotIn('warehouse_id', $stockByWarehouse->keys()) ->update(['stock' => 0]); } }