obtenerStockEnBodega($producto, $bodegaOrigen); if ($stockOrigen < $cantidad) { throw new Exception( "Stock insuficiente en {$bodegaOrigen->nombre}. " . "Disponible: {$stockOrigen}, Solicitado: {$cantidad}" ); } // Reducir stock en bodega origen $this->actualizarStockBodega($producto, $bodegaOrigen, $stockOrigen - $cantidad); // Aumentar stock en bodega destino $stockDestino = $this->obtenerStockEnBodega($producto, $bodegaDestino); $this->actualizarStockBodega($producto, $bodegaDestino, $stockDestino + $cantidad); // Crear registro de transferencia $transferencia = TransferenciaBodega::create([ 'producto_id' => $productoId, 'bodega_origen_id' => $bodegaOrigenId, 'bodega_destino_id' => $bodegaDestinoId, 'cantidad' => $cantidad, 'motivo' => $motivo, 'usuario_id' => Auth::id() ?? 1, // Fallback a usuario administrador si no hay autenticación 'fecha_transferencia' => now(), ]); DB::commit(); return $transferencia; } catch (Exception $e) { DB::rollBack(); throw $e; } } /** * Obtener el stock de un producto en una bodega específica * * @param Producto $producto * @param Bodega $bodega * @return int */ private function obtenerStockEnBodega(Producto $producto, Bodega $bodega): int { $relacion = $producto->bodegas()->where('bodega_id', $bodega->id)->first(); return $relacion ? $relacion->pivot->stock : 0; } /** * Actualizar el stock de un producto en una bodega * * @param Producto $producto * @param Bodega $bodega * @param int $nuevoStock * @return void */ private function actualizarStockBodega(Producto $producto, Bodega $bodega, int $nuevoStock): void { $existeRelacion = $producto->bodegas()->where('bodega_id', $bodega->id)->exists(); if ($existeRelacion) { // Actualizar relación existente $producto->bodegas()->updateExistingPivot($bodega->id, [ 'stock' => $nuevoStock ]); } else { // Crear nueva relación (solo si el stock es mayor a 0) if ($nuevoStock > 0) { $producto->bodegas()->attach($bodega->id, [ 'stock' => $nuevoStock ]); } } } /** * Obtener el historial de transferencias de un producto * * @param int $productoId * @param int $limit * @return \Illuminate\Database\Eloquent\Collection */ public function obtenerHistorialTransferencias(int $productoId, int $limit = 50) { return TransferenciaBodega::with(['bodegaOrigen', 'bodegaDestino', 'usuario']) ->where('producto_id', $productoId) ->orderBy('fecha_transferencia', 'desc') ->limit($limit) ->get(); } /** * Obtener resumen de stock por bodega para un producto * * @param int $productoId * @return array */ public function obtenerResumenStockPorBodega(int $productoId): array { $producto = Producto::with('bodegas')->findOrFail($productoId); $resumen = []; foreach ($producto->bodegas as $bodega) { $resumen[] = [ 'bodega_id' => $bodega->id, 'bodega_nombre' => $bodega->nombre, 'stock' => $bodega->pivot->stock, ]; } return $resumen; } /** * Obtener resumen de stock considerando variantes * * @param int $productoId * @return array */ public function obtenerResumenStockCompleto(int $productoId): array { $producto = Producto::with(['bodegas', 'variants.bodegas'])->findOrFail($productoId); $resumen = [ 'producto' => [ 'id' => $producto->id, 'nombre' => $producto->nombre, 'tiene_variantes' => $producto->variants()->exists(), 'stock_efectivo' => $producto->getStockEfectivo(), ], 'distribuciones' => [] ]; if ($producto->variants()->exists()) { // Producto con variantes foreach ($producto->variants as $variante) { $stockVariante = $variante->getStockEfectivo(); $distribucionVariante = [ 'variante_id' => $variante->id, 'sku' => $variante->sku, 'stock_total' => $stockVariante, 'bodegas' => [] ]; if ($variante->bodegas()->exists()) { foreach ($variante->bodegas as $bodega) { $distribucionVariante['bodegas'][] = [ 'bodega_id' => $bodega->id, 'bodega_nombre' => $bodega->nombre, 'stock' => $bodega->pivot->stock, ]; } } else { // Stock directo de la variante $distribucionVariante['stock_directo'] = $variante->stock; } $resumen['distribuciones'][] = $distribucionVariante; } } else { // Producto sin variantes if ($producto->bodegas()->exists()) { foreach ($producto->bodegas as $bodega) { $resumen['distribuciones'][] = [ 'bodega_id' => $bodega->id, 'bodega_nombre' => $bodega->nombre, 'stock' => $bodega->pivot->stock, ]; } } else { $resumen['stock_directo'] = $producto->stock; } } return $resumen; } /** * Transferir variantes entre bodegas * * @param int $varianteId * @param int $bodegaOrigenId * @param int $bodegaDestinoId * @param int $cantidad * @param string|null $motivo * @return TransferenciaBodega * @throws Exception */ public function transferirVariante( int $varianteId, int $bodegaOrigenId, int $bodegaDestinoId, int $cantidad, ?string $motivo = null ): TransferenciaBodega { if ($cantidad <= 0) { throw new Exception('La cantidad debe ser mayor a 0'); } if ($bodegaOrigenId === $bodegaDestinoId) { throw new Exception('La bodega de origen y destino no pueden ser la misma'); } $variante = ProductVariant::findOrFail($varianteId); $bodegaOrigen = Bodega::findOrFail($bodegaOrigenId); $bodegaDestino = Bodega::findOrFail($bodegaDestinoId); DB::beginTransaction(); try { // Verificar que la bodega origen tiene suficiente stock $stockOrigen = $this->obtenerStockVarianteEnBodega($variante, $bodegaOrigen); if ($stockOrigen < $cantidad) { throw new Exception( "Stock insuficiente en {$bodegaOrigen->nombre}. " . "Disponible: {$stockOrigen}, Solicitado: {$cantidad}" ); } // Reducir stock en bodega origen $this->actualizarStockVarianteBodega($variante, $bodegaOrigen, $stockOrigen - $cantidad); // Aumentar stock en bodega destino $stockDestino = $this->obtenerStockVarianteEnBodega($variante, $bodegaDestino); $this->actualizarStockVarianteBodega($variante, $bodegaDestino, $stockDestino + $cantidad); // Crear registro de transferencia $transferencia = TransferenciaBodega::create([ 'variante_id' => $varianteId, 'producto_id' => $variante->producto_id, 'bodega_origen_id' => $bodegaOrigenId, 'bodega_destino_id' => $bodegaDestinoId, 'cantidad' => $cantidad, 'motivo' => $motivo ? "Variante: {$motivo}" : "Transferencia de variante {$variante->sku}", 'usuario_id' => Auth::id() ?? 1, // Fallback a usuario administrador si no hay autenticación 'fecha_transferencia' => now(), ]); DB::commit(); return $transferencia; } catch (Exception $e) { DB::rollBack(); throw $e; } } /** * Obtener el stock de una variante en una bodega específica * * @param ProductVariant $variante * @param Bodega $bodega * @return int */ private function obtenerStockVarianteEnBodega(ProductVariant $variante, Bodega $bodega): int { $relacion = $variante->bodegas()->where('bodega_id', $bodega->id)->first(); return $relacion ? $relacion->pivot->stock : 0; } /** * Actualizar el stock de una variante en una bodega * * @param ProductVariant $variante * @param Bodega $bodega * @param int $nuevoStock * @return void */ private function actualizarStockVarianteBodega(ProductVariant $variante, Bodega $bodega, int $nuevoStock): void { $existeRelacion = $variante->bodegas()->where('bodega_id', $bodega->id)->exists(); if ($existeRelacion) { // Actualizar relación existente $variante->bodegas()->updateExistingPivot($bodega->id, [ 'stock' => $nuevoStock ]); } else { // Crear nueva relación (solo si el stock es mayor a 0) if ($nuevoStock > 0) { $variante->bodegas()->attach($bodega->id, [ 'stock' => $nuevoStock ]); } } } /** * Obtener resumen de stock por bodega para una variante * * @param int $varianteId * @return array */ public function obtenerResumenStockVariante(int $varianteId): array { $variante = ProductVariant::with(['bodegas', 'producto'])->findOrFail($varianteId); $resumen = [ 'variante_id' => $varianteId, 'variante_sku' => $variante->sku, 'producto_nombre' => $variante->producto->nombre, 'stock_total' => $variante->getStockEfectivo(), 'bodegas' => [] ]; if ($variante->bodegas()->exists()) { foreach ($variante->bodegas as $bodega) { $resumen['bodegas'][] = [ 'bodega_id' => $bodega->id, 'bodega_nombre' => $bodega->nombre, 'stock' => $bodega->pivot->stock, ]; } } else { $resumen['stock_directo'] = $variante->stock; } return $resumen; } }