375 lines
12 KiB
PHP
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;
|
|
}
|
|
} |