pdv.backend/app/Models/Inventory.php

184 lines
4.5 KiB
PHP

<?php
namespace App\Models;
/**
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use App\Services\InventoryMovementService;
use Illuminate\Database\Eloquent\Model;
/**
* Modelo de Inventario (Catálogo de productos)
*
* El stock NO vive aquí, vive en inventory_warehouse
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class Inventory extends Model
{
protected $fillable = [
'category_id',
'unit_of_measure_id',
'name',
'key_sat',
'sku',
'barcode',
'track_serials',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
'track_serials' => 'boolean',
];
protected $appends = ['has_serials', 'inventory_value', 'stock'];
public function warehouses()
{
return $this->belongsToMany(Warehouse::class, 'inventory_warehouse')
->withPivot('stock', 'min_stock', 'max_stock')
->withTimestamps();
}
/**
* Stock total en todos los almacenes (ahora soporta decimales)
*/
public function getStockAttribute(): float
{
return (float) $this->warehouses()->sum('inventory_warehouse.stock');
}
/**
* Alias para compatibilidad
*/
public function getTotalStockAttribute(): float
{
return $this->stock;
}
/**
* Stock en un almacén específico (ahora soporta decimales)
*/
public function stockInWarehouse(int $warehouseId): float
{
return (float) ($this->warehouses()
->where('warehouse_id', $warehouseId)
->value('inventory_warehouse.stock') ?? 0);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function unitOfMeasure()
{
return $this->belongsTo(UnitOfMeasurement::class);
}
public function price()
{
return $this->hasOne(Price::class);
}
public function serials()
{
return $this->hasMany(InventorySerial::class);
}
/**
* Obtener seriales disponibles
*/
public function availableSerials()
{
return $this->hasMany(InventorySerial::class)
->where('status', 'disponible');
}
public function unitEquivalences()
{
return $this->hasMany(UnitEquivalence::class);
}
/**
* Obtener factor de conversión para una unidad dada.
* Retorna 1.0 si es la unidad base del producto.
*/
public function getConversionFactor(int $unitOfMeasureId): float
{
if ($this->unit_of_measure_id === $unitOfMeasureId) {
return 1.0;
}
$equivalence = $this->unitEquivalences()
->where('unit_of_measure_id', $unitOfMeasureId)
->where('is_active', true)
->first();
if (! $equivalence) {
throw new \Exception(
"No existe equivalencia activa para la unidad seleccionada en el producto '{$this->name}'."
);
}
return (float) $equivalence->conversion_factor;
}
/**
* Convertir cantidad de cualquier unidad a unidades base
*/
public function convertToBaseUnits(float $quantity, int $unitOfMeasureId): float
{
return round($quantity * $this->getConversionFactor($unitOfMeasureId), 3);
}
/**
* Convertir costo por unidad de entrada a costo por unidad base
*/
public function convertCostToBaseUnit(float $costPerUnit, int $unitOfMeasureId): float
{
$factor = $this->getConversionFactor($unitOfMeasureId);
if ($factor <= 0) {
throw new \Exception('Factor de conversión inválido.');
}
return round($costPerUnit / $factor, 2);
}
/**
* Stock basado en seriales disponibles (para productos con track_serials)
*/
public function getAvailableStockAttribute(): int
{
return $this->availableSerials()->count();
}
public function getHasSerialsAttribute(): bool
{
return isset($this->attributes['serials_count']) && $this->attributes['serials_count'] > 0;
}
/**
* Valor total del inventario (stock * costo)
*/
public function getInventoryValueAttribute(): float
{
return $this->stock * ($this->price?->cost ?? 0);
}
/**
* Sincronizar stock basado en seriales disponibles
* Delega al servicio para mantener la lógica centralizada
*/
public function syncStock(): void
{
app(InventoryMovementService::class)->syncStockFromSerials($this);
}
}