Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
8.51% covered (danger)
8.51%
4 / 47
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
LowStockReport
8.51% covered (danger)
8.51%
4 / 47
50.00% covered (danger)
50.00%
4 / 8
212.04
0.00% covered (danger)
0.00%
0 / 1
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 label
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 group
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 filters
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 columns
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 rows
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
72
 summary
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Reports\Inventory;
4
5use App\Models\admin\Product;
6use App\Models\WarehouseStock;
7use App\Reports\Report;
8use App\Services\Location\BranchContext;
9use Illuminate\Support\Collection;
10
11class LowStockReport extends Report
12{
13    public function key(): string
14    {
15        return 'low-stock';
16    }
17
18    public function label(): string
19    {
20        return 'Low Stock / Reorder';
21    }
22
23    public function group(): string
24    {
25        return 'Inventory';
26    }
27
28    public function description(): ?string
29    {
30        return 'Products at or below their configured low-stock alert level, or already marked out of stock.';
31    }
32
33    public function filters(): array
34    {
35        if (! feature('multi_warehouse')) {
36            return [];
37        }
38
39        return ['warehouse_id' => [
40            'type' => 'select', 'label' => 'Warehouse', 'default' => '',
41            'options' => ['' => 'All warehouses'] + app(BranchContext::class)->warehouses()->active()->orderBy('name')->pluck('name', 'id')->all(),
42        ]];
43    }
44
45    public function columns(): array
46    {
47        return [
48            ['key' => 'name', 'label' => 'Product'],
49            ['key' => 'category', 'label' => 'Category'],
50            ['key' => 'stock', 'label' => 'On Hand', 'align' => 'right'],
51            ['key' => 'alert_quantity', 'label' => 'Alert At', 'align' => 'right'],
52            ['key' => 'status', 'label' => 'Status'],
53        ];
54    }
55
56    public function rows(array $filters): Collection
57    {
58        $warehouseId = $filters['warehouse_id'] ?? null;
59        $branchContext = app(BranchContext::class);
60        $branchContext->ensureWarehouse($warehouseId ? (int) $warehouseId : null);
61        $branchId = $branchContext->id();
62        $useLocationTotals = (bool) ($warehouseId || $branchId);
63        $locationTotals = $useLocationTotals
64            ? WarehouseStock::query()
65                ->when($warehouseId, fn ($query) => $query->where('warehouse_id', $warehouseId))
66                ->when(! $warehouseId && $branchId, fn ($query) => $query->whereHas('warehouse', fn ($warehouse) => $warehouse->where('branch_id', $branchId)))
67                ->selectRaw('product_id, SUM(quantity) quantity')->groupBy('product_id')->pluck('quantity', 'product_id')
68            : collect();
69
70        return Product::query()
71            ->with('category')
72            ->when(! $useLocationTotals, function ($query) {
73                $query->where(function ($q) {
74                    $q->where('stock_status', 'out_of_stock')->orWhereColumn('stock', '<=', 'alert_quantity');
75                });
76            })
77            ->orderBy('stock')
78            ->get()
79            ->map(fn (Product $p) => (object) [
80                'name' => $p->name,
81                'category' => $p->category->name ?? '—',
82                'stock' => (float) ($useLocationTotals ? ($locationTotals[$p->id] ?? 0) : $p->stock),
83                'alert_quantity' => (int) ($p->alert_quantity ?? 5),
84                'status' => (float) ($useLocationTotals ? ($locationTotals[$p->id] ?? 0) : $p->stock) > 0 ? 'Low stock' : 'Out of stock',
85            ])->filter(fn ($row) => $row->stock <= $row->alert_quantity)->values();
86    }
87
88    public function summary(Collection $rows, array $filters): array
89    {
90        return [
91            'Products needing reorder' => number_format($rows->count()),
92        ];
93    }
94}