diff --git a/app/Models/Item.php b/app/Models/Item.php index 1fac2aa73..c413f3b1c 100644 --- a/app/Models/Item.php +++ b/app/Models/Item.php @@ -258,8 +258,13 @@ class Item extends Model // Order by name of item by default if ($sort === 'quantity' && $filters['stock_location_id'] <= -1) { - $idBuilder->join('item_quantities AS item_quantities', 'item_quantities.item_id = items.item_id', 'left'); - $idBuilder->orderBy('SUM(item_quantities.quantity)', $order); + $itemQuantitiesTable = $this->db->prefixTable('item_quantities'); + $idBuilder->join( + "(SELECT item_id, SUM(quantity) AS total_quantity FROM $itemQuantitiesTable GROUP BY item_id) AS item_quantity_totals", + 'item_quantity_totals.item_id = items.item_id', + 'left' + ); + $idBuilder->orderBy('item_quantity_totals.total_quantity', $order); } else { $idBuilder->orderBy($sort, $order); } @@ -314,7 +319,7 @@ class Item extends Model $builder->select('MAX(item_quantities.location_id) AS location_id'); $builder->select('MAX(item_quantities.quantity) AS quantity'); } elseif ($sortByQuantityAllLocations) { - $builder->select('SUM(item_quantities.quantity) AS quantity'); + $builder->select('item_quantity_totals.total_quantity AS quantity'); } $builder->join('suppliers AS suppliers', 'suppliers.person_id = items.supplier_id', 'left'); @@ -324,7 +329,12 @@ class Item extends Model $builder->join('item_quantities AS item_quantities', 'item_quantities.item_id = items.item_id'); $builder->where('location_id', $filters['stock_location_id']); } elseif ($sortByQuantityAllLocations) { - $builder->join('item_quantities AS item_quantities', 'item_quantities.item_id = items.item_id', 'left'); + $itemQuantitiesTable = $this->db->prefixTable('item_quantities'); + $builder->join( + "(SELECT item_id, SUM(quantity) AS total_quantity FROM $itemQuantitiesTable GROUP BY item_id) AS item_quantity_totals", + 'item_quantity_totals.item_id = items.item_id', + 'left' + ); } $applyTransDateRange($builder); @@ -344,7 +354,7 @@ class Item extends Model // Re-apply order: WHERE...IN + GROUP BY do not preserve Phase A's row order if ($sortByQuantityAllLocations) { - $builder->orderBy('SUM(item_quantities.quantity)', $order); + $builder->orderBy('item_quantity_totals.total_quantity', $order); } else { $builder->orderBy($sort, $order); } diff --git a/tests/Models/ItemSearchTest.php b/tests/Models/ItemSearchTest.php index 9287ac757..0fb2f18ea 100644 --- a/tests/Models/ItemSearchTest.php +++ b/tests/Models/ItemSearchTest.php @@ -190,4 +190,31 @@ class ItemSearchTest extends CIUnitTestCase $this->assertCount(1, $resultsWhenTemporary); $this->assertCount(0, $resultsWhenNotTemporary); } + + public function testSearchSortByQuantitySumsAcrossLocationsWithoutMultiplication(): void + { + $itemId = $this->createSearchableItem(['name' => 'Multi Location Item ' . uniqid()]); + + // createSearchableItem already inserts one inventory row; add two more + // inventory transactions so a naive SUM-after-join (which multiplies + // item_quantities rows by the inventory join) would triple-count. + $this->addInventoryRecord($itemId, 1); + $this->addInventoryRecord($itemId, 1); + + $this->addItemQuantity($itemId, 1, 10); + + $results = $this->item->search( + '', + $this->defaultSearchFilters(), + 0, + 0, + 'quantity', + 'desc' + )->getResult(); + + $match = array_values(array_filter($results, static fn ($r) => (int) $r->item_id === $itemId)); + + $this->assertCount(1, $match); + $this->assertEquals(10, (float) $match[0]->quantity); + } }