feat: agregar cálculo del valor total del inventario y filtros adicionales en el controlador de inventario

This commit is contained in:
Juan Felipe Zapata Moreno 2026-02-05 12:01:25 -06:00
parent a0e8c70624
commit 656492251b
4 changed files with 30 additions and 37 deletions

View File

@ -8,6 +8,7 @@
use App\Services\ProductService;
use App\Imports\ProductsImport;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Notsoweb\ApiResponse\Enums\ApiResponse;
use Maatwebsite\Excel\Facades\Excel;
use Maatwebsite\Excel\Validators\ValidationException;
@ -27,6 +28,7 @@ public function index(Request $request)
->where('is_active', true);
// Filtro por búsqueda de texto (nombre, SKU, código de barras)
if ($request->has('q') && $request->q) {
$products->where(function($query) use ($request) {
$query->where('name', 'like', "%{$request->q}%")
@ -35,11 +37,22 @@ public function index(Request $request)
});
}
// Filtro por categoría (independiente de la búsqueda de texto)
if ($request->has('category_id') && $request->category_id) {
$products->where('category_id', $request->category_id);
}
// Calcular el valor total del inventario
$totalInventoryValue = Inventory::join('prices', 'inventories.id', '=', 'prices.inventory_id')
->where('inventories.is_active', true)
->sum(DB::raw('inventories.stock * prices.cost'));
$products = $products->orderBy('name')
->paginate(config('app.pagination'));
return ApiResponse::OK->response([
'products' => $products
'products' => $products,
'total_inventory_value' => round($totalInventoryValue, 2)
]);
}

View File

@ -61,11 +61,9 @@ public function topSellingProduct(Request $request)
public function productsWithoutMovement(Request $request)
{
$request->validate([
'include_stock_value' => ['nullable', 'boolean'],
'from_date' => ['required', 'date_format:Y-m-d'],
'to_date' => ['required', 'date_format:Y-m-d', 'after_or_equal:from_date'],
], [
'include_stock_value.boolean' => 'El parámetro de valor de stock debe ser verdadero o falso.',
'from_date.required' => 'La fecha inicial es obligatoria.',
'from_date.date_format' => 'La fecha inicial debe tener el formato Y-m-d (ejemplo: 2025-01-01).',
'to_date.required' => 'La fecha final es obligatoria.',
@ -75,14 +73,12 @@ public function productsWithoutMovement(Request $request)
try {
$products = $this->reportService->getProductsWithoutMovement(
includeStockValue: (bool)($request->input('include_stock_value', true)),
fromDate: $request->input('from_date'),
toDate: $request->input('to_date')
);
return ApiResponse::OK->response([
'products' => $products,
'total_products' => count($products),
'message' => 'Reporte de productos sin movimiento generado exitosamente.'
]);
} catch (\Exception $e) {

View File

@ -30,7 +30,7 @@ class Inventory extends Model
'track_serials' => 'boolean',
];
protected $appends = ['has_serials'];
protected $appends = ['has_serials', 'inventory_value'];
public function category()
{
@ -78,4 +78,12 @@ public function getHasSerialsAttribute(): bool
{
return isset($this->attributes['serials_count']) && $this->attributes['serials_count'] > 0;
}
/**
* Calcular el valor total del inventario para este producto (stock * costo)
*/
public function getInventoryValueAttribute(): float
{
return $this->stock * ($this->price?->cost ?? 0);
}
}

View File

@ -51,12 +51,8 @@ public function getTopSellingProduct(?string $fromDate = null, ?string $toDate =
/**
* Obtener productos sin movimiento
*
* @param int $daysThreshold Días mínimos sin movimiento (default: 30)
* @param bool $includeStockValue Incluir valor del inventario (default: true)
* @return array Lista de productos sin ventas
*/
public function getProductsWithoutMovement(bool $includeStockValue = true, ?string $fromDate = null, ?string $toDate = null): array
public function getProductsWithoutMovement(?string $fromDate = null, ?string $toDate = null)
{
// Obtener IDs de productos que SÍ tienen ventas
$inventoriesWithSales = SaleDetail::query()
@ -68,33 +64,13 @@ public function getProductsWithoutMovement(bool $includeStockValue = true, ?stri
->pluck('sale_details.inventory_id')
->toArray();
// Construir query para productos SIN ventas
// Construir query para productos SIN ventas usando relaciones de Eloquent
$query = Inventory::query()
->selectRaw('
inventories.id,
inventories.name,
inventories.sku,
inventories.stock,
categories.name as category_name,
inventories.created_at as date_added,
prices.retail_price
');
->with(['category', 'price'])
->where('is_active', true)
->whereNotIn('id', $inventoriesWithSales)
->orderBy('created_at');
// Agregar valor del inventario si se solicita
if ($includeStockValue) {
$query->addSelect(
DB::raw('(inventories.stock * COALESCE(prices.cost, 0)) as inventory_value')
);
}
return $query
->leftJoin('categories', 'inventories.category_id', '=', 'categories.id')
->leftJoin('prices', 'inventories.id', '=', 'prices.inventory_id')
->where('inventories.is_active', true)
->whereNull('inventories.deleted_at')
->whereNotIn('inventories.id', $inventoriesWithSales)
->orderBy('inventories.created_at')
->get()
->toArray();
return $query->paginate(config('app.pagination', 10));
}
}