'decimal:2', 'qty_reserved' => 'decimal:3', 'min_qty' => 'decimal:2', 'buy_price' => 'decimal:2', 'sell_price' => 'decimal:2', 'is_active' => 'boolean', ]; public function preferredSupplier(): BelongsTo { return $this->belongsTo(Supplier::class, 'preferred_supplier_id'); } public function batches(): HasMany { return $this->hasMany(PartBatch::class); } public function reservations(): HasMany { return $this->hasMany(PartReservation::class); } public function events(): HasMany { return $this->hasMany(WarehouseEvent::class); } /** Live total across all batches of all warehouses (source of truth). */ public function qtyOnHand(?int $warehouseId = null): float { $q = $this->batches()->newQuery()->where('part_id', $this->id); if ($warehouseId) $q->where('warehouse_id', $warehouseId); return (float) $q->sum('qty_remaining'); } /** Available for new reservations = on hand − already reserved. */ public function qtyAvailable(?int $warehouseId = null): float { return max(0.0, $this->qtyOnHand($warehouseId) - (float) $this->qty_reserved); } public function isLow(): bool { return (float) $this->qty <= (float) $this->min_qty; } public function isOut(): bool { return (float) $this->qty <= 0; } /** * Legacy direct-stock adjustment. * NOTE: this only moves the cached `qty` column. Real stock changes * should go through WarehouseService so batches + events stay in sync. */ public function adjustStock(float $delta, ?string $reason = null): void { $this->qty = max(0, (float) $this->qty + $delta); $this->save(); } }