Overview
The inventory management system consists of two main models: InventoryStock for tracking current stock levels and InventoryMovement for recording all inventory transactions.
InventoryStock Model
Tracks current stock quantities for each product in each warehouse.
Namespace: App\Models\Inventory\InventoryStock
Database Table: inventory_stocks
Traits: HasFactory
Properties
Foreign key to the Warehouse model
Foreign key to the Product model
Current quantity in stock
Minimum stock level threshold for low stock alerts
Relationships
warehouse()
Returns the warehouse where this stock is located
$stock->warehouse; // Warehouse object
$stock->warehouse->name; // Warehouse name
product()
Returns the product this stock record represents
$stock->product; // Product object
$stock->product->name; // Product name
$stock->product->sku; // Product SKU
Usage Examples
Checking Stock Levels
use App\Models\Inventory\InventoryStock;
// Get stock for a product in a specific warehouse
$stock = InventoryStock::where('product_id', $productId)
->where('warehouse_id', $warehouseId)
->first();
if ($stock && $stock->quantity > 0) {
echo "Available: {$stock->quantity}";
} else {
echo "Out of stock";
}
Low Stock Alerts
// Get all low stock items
$lowStockItems = InventoryStock::whereRaw('quantity <= min_stock')
->with(['product', 'warehouse'])
->get();
foreach ($lowStockItems as $item) {
echo "{$item->product->name} is low in {$item->warehouse->name}";
echo "Current: {$item->quantity}, Minimum: {$item->min_stock}";
}
Stock by Warehouse
// Get all products in a warehouse
$warehouseStock = InventoryStock::where('warehouse_id', $warehouseId)
->with('product')
->where('quantity', '>', 0)
->get();
// Calculate total inventory value
$totalValue = $warehouseStock->sum(function ($stock) {
return $stock->quantity * $stock->product->cost;
});
InventoryMovement Model
Records all inventory transactions including inputs, outputs, adjustments, and transfers.
Namespace: App\Models\Inventory\InventoryMovement
Database Table: inventory_movements
Traits: SoftDeletes
Properties
Foreign key to the source warehouse
Foreign key to destination warehouse (for transfers only)
Foreign key to the Product model
Foreign key to the User who created the movement
Quantity moved (positive for inputs, negative for outputs)
Movement type: ‘input’, ‘output’, ‘adjustment’, or ‘transfer’
Stock level before this movement (audit trail)
Stock level after this movement (audit trail)
Description or reason for the movement
Polymorphic type for the related document (Sale, Purchase, etc.)
Polymorphic ID for the related document
Constants
const TYPE_INPUT = 'input';
const TYPE_OUTPUT = 'output';
const TYPE_ADJUSTMENT = 'adjustment';
const TYPE_TRANSFER = 'transfer';
Relationships
warehouse()
Returns the source warehouse
$movement->warehouse; // Source Warehouse object
toWarehouse()
Returns the destination warehouse (for transfers)
$movement->toWarehouse; // Destination Warehouse object (or null)
user()
Returns the user who created the movement
$movement->user; // User object
$movement->user->name; // User name
product()
Returns the product being moved
$movement->product; // Product object
reference()
Returns the related document (Sale, Purchase, etc.)
$movement->reference; // Could be Sale, Purchase, or other model
Accessors
typeLabel
Returns human-readable label for the movement type.
$movement->typeLabel; // 'Entrada', 'Salida', 'Ajuste', or 'Transferencia'
Scopes
withIndexRelations()
Eager loads all relationships for list views.
InventoryMovement::withIndexRelations()->get();
This scope loads:
warehouse
toWarehouse
user
product
reference
Static Methods
getTypes()
Returns array of movement types with labels.
InventoryMovement::getTypes();
// Returns:
// [
// 'input' => 'Entrada',
// 'output' => 'Salida',
// 'adjustment' => 'Ajuste',
// 'transfer' => 'Transferencia'
// ]
Usage Examples
Recording an Input
use App\Models\Inventory\InventoryMovement;
$movement = InventoryMovement::create([
'warehouse_id' => $warehouseId,
'product_id' => $productId,
'user_id' => auth()->id(),
'quantity' => 100,
'type' => InventoryMovement::TYPE_INPUT,
'previous_stock' => $currentStock,
'current_stock' => $currentStock + 100,
'description' => 'Purchase order receipt',
'reference_type' => 'App\\Models\\Purchases\\Purchase',
'reference_id' => $purchase->id
]);
Recording an Output (Sale)
$movement = InventoryMovement::create([
'warehouse_id' => $warehouseId,
'product_id' => $productId,
'user_id' => auth()->id(),
'quantity' => -10, // Negative for outputs
'type' => InventoryMovement::TYPE_OUTPUT,
'previous_stock' => $currentStock,
'current_stock' => $currentStock - 10,
'description' => 'Sale to customer',
'reference_type' => 'App\\Models\\Sales\\Sale',
'reference_id' => $sale->id
]);
Recording a Transfer
$movement = InventoryMovement::create([
'warehouse_id' => $fromWarehouseId,
'to_warehouse_id' => $toWarehouseId,
'product_id' => $productId,
'user_id' => auth()->id(),
'quantity' => 50,
'type' => InventoryMovement::TYPE_TRANSFER,
'previous_stock' => $currentStock,
'current_stock' => $currentStock - 50,
'description' => 'Transfer to branch warehouse'
]);
Recording an Adjustment
// Stock count revealed discrepancy
$expectedStock = 100;
$actualStock = 95;
$adjustment = $actualStock - $expectedStock; // -5
$movement = InventoryMovement::create([
'warehouse_id' => $warehouseId,
'product_id' => $productId,
'user_id' => auth()->id(),
'quantity' => $adjustment,
'type' => InventoryMovement::TYPE_ADJUSTMENT,
'previous_stock' => $expectedStock,
'current_stock' => $actualStock,
'description' => 'Physical inventory adjustment - damaged units'
]);
Querying Movement History
// Get all movements for a product
$productHistory = InventoryMovement::where('product_id', $productId)
->withIndexRelations()
->orderBy('created_at', 'desc')
->get();
foreach ($productHistory as $movement) {
echo $movement->typeLabel;
echo $movement->quantity;
echo $movement->warehouse->name;
echo $movement->user->name;
}
Filtering by Movement Type
// Get all inputs this month
$inputs = InventoryMovement::where('type', InventoryMovement::TYPE_INPUT)
->whereMonth('created_at', now()->month)
->with('product', 'warehouse')
->get();
// Get all transfers between specific warehouses
$transfers = InventoryMovement::where('type', InventoryMovement::TYPE_TRANSFER)
->where('warehouse_id', $fromWarehouse)
->where('to_warehouse_id', $toWarehouse)
->get();
Movement Audit Trail
// Get movement history with audit data
$movements = InventoryMovement::where('product_id', $productId)
->where('warehouse_id', $warehouseId)
->orderBy('created_at', 'desc')
->get();
foreach ($movements as $movement) {
echo "Previous: {$movement->previous_stock}";
echo "Change: {$movement->quantity}";
echo "Current: {$movement->current_stock}";
echo "By: {$movement->user->name}";
echo "Date: {$movement->created_at}";
}
Movement Reports
// Daily movement summary
$dailySummary = InventoryMovement::whereDate('created_at', today())
->selectRaw('type, COUNT(*) as count, SUM(ABS(quantity)) as total_quantity')
->groupBy('type')
->get();
// Product movement by warehouse
$warehouseMovements = InventoryMovement::where('product_id', $productId)
->selectRaw('warehouse_id, type, SUM(quantity) as total')
->groupBy('warehouse_id', 'type')
->with('warehouse')
->get();
Related Document Access
// Access the related sale or purchase
if ($movement->reference_type === 'App\\Models\\Sales\\Sale') {
$sale = $movement->reference; // Polymorphic relationship
echo "Related to Sale: {$sale->number}";
}
Complete Inventory Flow Example
Receiving Products (Input)
use App\Models\Inventory\InventoryStock;
use App\Models\Inventory\InventoryMovement;
use Illuminate\Support\Facades\DB;
DB::transaction(function () use ($warehouseId, $productId, $quantity, $purchaseId) {
// Get or create stock record
$stock = InventoryStock::firstOrCreate(
[
'warehouse_id' => $warehouseId,
'product_id' => $productId
],
['quantity' => 0, 'min_stock' => 10]
);
$previousStock = $stock->quantity;
$stock->quantity += $quantity;
$stock->save();
// Record the movement
InventoryMovement::create([
'warehouse_id' => $warehouseId,
'product_id' => $productId,
'user_id' => auth()->id(),
'quantity' => $quantity,
'type' => InventoryMovement::TYPE_INPUT,
'previous_stock' => $previousStock,
'current_stock' => $stock->quantity,
'description' => 'Purchase receipt',
'reference_type' => 'App\\Models\\Purchases\\Purchase',
'reference_id' => $purchaseId
]);
});
Processing a Sale (Output)
DB::transaction(function () use ($warehouseId, $productId, $quantity, $saleId) {
$stock = InventoryStock::where('warehouse_id', $warehouseId)
->where('product_id', $productId)
->firstOrFail();
if ($stock->quantity < $quantity) {
throw new \Exception('Insufficient stock');
}
$previousStock = $stock->quantity;
$stock->quantity -= $quantity;
$stock->save();
InventoryMovement::create([
'warehouse_id' => $warehouseId,
'product_id' => $productId,
'user_id' => auth()->id(),
'quantity' => -$quantity,
'type' => InventoryMovement::TYPE_OUTPUT,
'previous_stock' => $previousStock,
'current_stock' => $stock->quantity,
'description' => 'Sale to customer',
'reference_type' => 'App\\Models\\Sales\\Sale',
'reference_id' => $saleId
]);
});
Related Models
- Product - Products being tracked
- Warehouse - Storage locations
- Sale - Sales that reduce inventory
- User - Users making movements