stock; $currentCost = $inventory->price?->cost ?? 0.00; // Calcular nuevo costo promedio ponderado $newCost = $this->calculateWeightedAverageCost( $curentStock, $currentCost, $quantity, $unitCost ); // Actualizar costo en prices $this->updateProductCost($inventory, $newCost); // 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, '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 = $productData['quantity']; $unitCost = $productData['unit_cost']; // 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); // Actualizar stock en inventory_warehouse $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::findOrFail($productData['inventory_id']); $quantity = $productData['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 $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 = $productData['quantity']; // 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 $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( int $currentStock, float $currentCost, int $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 = $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]); } }