- Migrar manejo de stock de a . - Implementar para centralizar lógica de entradas, salidas y transferencias. - Añadir (CRUD) y Requests de validación. - Actualizar reportes, cálculos de valor y migraciones para la nueva estructura. - Agregar campo para rastreo de movimientos.
231 lines
7.7 KiB
PHP
231 lines
7.7 KiB
PHP
<?php namespace App\Services;
|
|
|
|
use App\Models\Inventory;
|
|
use App\Models\InventoryMovement;
|
|
use App\Models\InventoryWarehouse;
|
|
use App\Models\Warehouse;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Servicio para gestión de movimientos de inventario
|
|
*
|
|
* El stock vive en inventory_warehouse, no en inventories
|
|
*/
|
|
class InventoryMovementService
|
|
{
|
|
/**
|
|
* Entrada de inventario (compra, ajuste positivo)
|
|
*/
|
|
public function entry(array $data): InventoryMovement
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
$inventory = Inventory::findOrFail($data['inventory_id']);
|
|
$warehouse = Warehouse::findOrFail($data['warehouse_id']);
|
|
$quantity = $data['quantity'];
|
|
|
|
// Actualizar stock en inventory_warehouse
|
|
$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,
|
|
'user_id' => auth()->id(),
|
|
'notes' => $data['notes'] ?? null,
|
|
'invoice_reference' => $data['invoice_reference'] ?? null,
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 = $data['quantity'];
|
|
|
|
// Validar stock disponible
|
|
$this->validateStock($inventory->id, $warehouse->id, $quantity);
|
|
|
|
// Decrementar stock en inventory_warehouse
|
|
$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::findOrFail($data['inventory_id']);
|
|
$warehouseFrom = Warehouse::findOrFail($data['warehouse_from_id']);
|
|
$warehouseTo = Warehouse::findOrFail($data['warehouse_to_id']);
|
|
$quantity = $data['quantity'];
|
|
|
|
// Validar que no sea el mismo almacén
|
|
if ($warehouseFrom->id === $warehouseTo->id) {
|
|
throw new \Exception('No se puede traspasar al mismo almacén.');
|
|
}
|
|
|
|
// Validar stock disponible en almacén origen
|
|
$this->validateStock($inventory->id, $warehouseFrom->id, $quantity);
|
|
|
|
// Decrementar en origen
|
|
$this->updateWarehouseStock($inventory->id, $warehouseFrom->id, -$quantity);
|
|
|
|
// Incrementar en destino
|
|
$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, int $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, int $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, int $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, int $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;
|
|
}
|
|
|
|
/**
|
|
* 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]);
|
|
}
|
|
}
|