Files
pos_heidiver/app/Services/TransferenciaBodegaService.php
T
2026-01-06 15:35:59 -05:00

375 lines
12 KiB
PHP

<?php
namespace App\Services;
use App\Models\Producto;
use App\Models\ProductVariant;
use App\Models\Bodega;
use App\Models\TransferenciaBodega;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Exception;
class TransferenciaBodegaService
{
/**
* Realizar transferencia de stock entre bodegas
*
* @param int $productoId
* @param int $bodegaOrigenId
* @param int $bodegaDestinoId
* @param int $cantidad
* @param string|null $motivo
* @return TransferenciaBodega
* @throws Exception
*/
public function transferir(
int $productoId,
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');
}
$producto = Producto::findOrFail($productoId);
$bodegaOrigen = Bodega::findOrFail($bodegaOrigenId);
$bodegaDestino = Bodega::findOrFail($bodegaDestinoId);
DB::beginTransaction();
try {
// Verificar que la bodega origen tiene suficiente stock
$stockOrigen = $this->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;
}
}