diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 41b3f9781c8a0..e983ec062751d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -1735,7 +1735,9 @@ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) if ($attribute == 'price' && $storeId != 0) { $this->addPriceData(); if ($this->_productLimitationFilters->isUsingPriceIndex()) { - $this->getSelect()->order("price_index.min_price {$dir}"); + $this->getSelect()->order( + new \Zend_Db_Expr("price_index.min_price = 0, price_index.min_price {$dir}") + ); return $this; } } diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 4693b7502c5c1..54b8c1966ee12 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -69,10 +69,22 @@ public function apply() foreach ($items as $item) { $ids[] = (int)$item->getId(); } - $this->collection->getSelect()->where('e.entity_id IN (?)', $ids); - $orderList = join(',', $ids); - $this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER); - $this->collection->getSelect()->order(new \Zend_Db_Expr("FIELD(e.entity_id,$orderList)")); + $this->collection->getSelect() + ->where('e.entity_id IN (?)', $ids) + ->reset(\Magento\Framework\DB\Select::ORDER); + $sortOrder = $this->searchResult->getSearchCriteria() + ->getSortOrders(); + if (!empty($sortOrder['price']) && $this->collection->getLimitationFilters()->isUsingPriceIndex()) { + $sortDirection = $sortOrder['price']; + $this->collection->getSelect() + ->order( + new \Zend_Db_Expr("price_index.min_price = 0, price_index.min_price {$sortDirection}") + ); + } else { + $orderList = join(',', $ids); + $this->collection->getSelect() + ->order(new \Zend_Db_Expr("FIELD(e.entity_id,$orderList)")); + } } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php index 7bebf9c49d14b..31da969540d30 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php @@ -110,16 +110,7 @@ public function testProductListSortOrder(string $sortBy, string $direction, arra */ public function testProductListSortOrderWithConfig(string $sortBy, string $direction, array $expectation): void { - $this->objectManager->removeSharedInstance(Config::class); - $this->scopeConfig->setValue( - Config::XML_PATH_LIST_DEFAULT_SORT_BY, - $sortBy, - ScopeInterface::SCOPE_STORE, - Store::DEFAULT_STORE_ID - ); - $category = $this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, null); - $this->renderBlock($category, $direction); - $this->assertBlockSorting($sortBy, $expectation); + $this->assertProductListSortOrderWithConfig($sortBy, $direction, $expectation); } /** @@ -322,4 +313,89 @@ private function updateCategorySortBy( return $category; } + + /** + * Test product list ordered by price with out-of-stock configurable product options with elasticsearch engine + * + * @magentoDataFixture Magento/Catalog/_files/products_with_not_empty_layered_navigation_attribute.php + * @magentoDataFixture Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child.php + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoConfigFixture default/catalog/search/engine elasticsearch7 + * @dataProvider productListWithOutOfStockSortOrderDataProvider + * @param string $sortBy + * @param string $direction + * @param array $expected + * @return void + */ + public function testProductListOutOfStockSortOrderWithElasticsearch( + string $sortBy, + string $direction, + array $expected + ): void { + $this->assertProductListSortOrderWithConfig($sortBy, $direction, $expected); + } + + /** + * Test product list ordered by price with out-of-stock configurable product options with mysql search engine + * + * @magentoDataFixture Magento/Catalog/_files/products_with_not_empty_layered_navigation_attribute.php + * @magentoDataFixture Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child.php + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoConfigFixture default/catalog/search/engine mysql + * @dataProvider productListWithOutOfStockSortOrderDataProvider + * @param string $sortBy + * @param string $direction + * @param array $expected + * @return void + */ + public function testProductListOutOfStockSortOrderWithMysql( + string $sortBy, + string $direction, + array $expected + ): void { + $this->assertProductListSortOrderWithConfig($sortBy, $direction, $expected); + } + + /** + * Product list with out-of-stock sort order data provider + * + * @return array + */ + public function productListWithOutOfStockSortOrderDataProvider(): array + { + return [ + 'default_order_price_asc' => [ + 'sort' => 'price', + 'direction' => Collection::SORT_ORDER_ASC, + 'expectation' => ['simple1', 'simple2', 'simple3', 'configurable'], + ], + 'default_order_price_desc' => [ + 'sort' => 'price', + 'direction' => Collection::SORT_ORDER_DESC, + 'expectation' => ['simple3', 'simple2', 'simple1', 'configurable'], + ], + ]; + } + + /** + * Assert product list order + * + * @param string $sortBy + * @param string $direction + * @param array $expected + * @return void + */ + private function assertProductListSortOrderWithConfig(string $sortBy, string $direction, array $expected): void + { + $this->objectManager->removeSharedInstance(Config::class); + $this->scopeConfig->setValue( + Config::XML_PATH_LIST_DEFAULT_SORT_BY, + $sortBy, + ScopeInterface::SCOPE_STORE, + Store::DEFAULT_STORE_ID + ); + $category = $this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, null); + $this->renderBlock($category, $direction); + $this->assertBlockSorting($sortBy, $expected); + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child.php new file mode 100644 index 0000000000000..e46228e52a117 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child.php @@ -0,0 +1,43 @@ +create(ProductRepositoryInterface::class); + +$product = $productRepository->get('simple_1010'); +$product->setStockData( + [ + 'qty' => 0, + ] +); +$productRepository->save($product); + +$product = $productRepository->get('simple_1020'); +$product->setStockData( + [ + 'qty' => 0, + ] +); +$productRepository->save($product); + +/** @var CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); +/** @var DefaultCategory $categoryHelper */ +$categoryHelper = $objectManager->get(DefaultCategory::class); + +foreach (['simple_1010', 'simple_1020', 'configurable'] as $sku) { + $categoryLinkManagement->assignProductToCategories($sku, [$categoryHelper->getId(), 333]); +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child_rollback.php new file mode 100644 index 0000000000000..05b0d8959a5d3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_out-of-stock_child_rollback.php @@ -0,0 +1,9 @@ +