Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX: out-of-stock options for configurable product visible on frontend as sellable #12936

Conversation

coderimus
Copy link
Contributor

@coderimus coderimus commented Jan 1, 2018

This PR is for configurable product out-of-stock option fix with several improvements for generally used methods. Please, check Description and Code changes explanation sections for more detail information.

Description

If one of the configurable product options will be out-of-stock but has status enabled, it will be visible on the frontend (product page with swatches).
But, when a customer selects it and tries to add to the cart the next message will be shown: "You need to choose options for your item." And item will not be added to the cart.
The main problem here that the out-of-stock and not salable option is shown as normal one without any notification before adding to the cart and when a customer tries to add it to the cart this generates useless load on the system.

Fixed Issues (if relevant)

The most relevant issue is #5948
Despite the fact that this issue was closed, it still can be reproduced on the Magento2 2.2.2.

Manual testing scenarios

  1. Select configurable product using swatches
  2. Choose one random option (simple product) and change it's qty to 0. You will get status "Out-of-stock"
  3. Go to the product page, reload it
  4. Select option with out-of-stock status and press "Add to cart button"
  5. The page will be reloaded and error message will be shown

Accepted criteria: out-of-stock option should be shown as not-selectable and there is no ability to add it to the cart.

Code changes explanation

  1. Magento\ConfigurableProduct\Model\Product\Type\Configurable::loadUsedProducts(\Magento\Catalog\Model\Product $product, $cacheKey, $salableOnly = false) method had 2 issues:
  • the $salableOnly variable never used since 100.2.0 because of deprecated method getSalableUsedProducts and, as a result, the stock status filter was not added to the collection object
  • even if the $salableOnly filter will be true and $this->salableProcessor->process($collection) will be executed, it will not get any effect because items were previously loaded in $collection = $this->getConfiguredUsedProductCollection($product); It means that collection will not be updated with the real stock status data.
    Items ($this->_items) were loaded before by $this->getItems() method in $collection->addMediaGalleryData() and $collection->addTierPriceData().
    This is why I have moved $collection->addMediaGalleryData() and $collection->addTierPriceData() to another method and execute them after all other collection modifications.

Also, I removed the if($salableOnly) {...} condition because there is no need to skip it. With other provided changes product collection will contain all products with correct stock statuses. There will not be skipped products which is equal to current behaviour.

The $collection->setFlag('has_stock_status_filter',false); line was added to add stock status filter.

  1. Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor::process(Collection $collection, $isFilterInStock = true) has 3 improvements:
  • This method now has second param $isFilterInStock with default value equals to true. It makes it more flexible because now you can just add stock status to collection or filter it by status equals to 1 (in stock)

  • !$collection->hasFlag($stockFlag) || !$collection->getFlag($stockFlag) this change allow develoeprs to check what value has collcetion flag. This was added because the hasFlag()' method checks only if current key exists - array_key_exists($flag, $this->_flags);`

  • $stockStatusResource->addStockDataToCollection($collection, $isFilterInStock); now uses $isFilterInStock value as parametr

  1. Magento\CatalogInventory\Model\ResourceModel\Stock\Status has next changes
  • addStockStatusToSelect, addStockDataToCollection, addIsInStockFilterToCollection now have $websiteId with null as default value and $this->getWebsiteId($websiteId) method for website condition check

Contribution checklist

  • Pull request has a meaningful description of its purpose
  • All commits are accompanied by meaningful commit messages
  • All new or changed code is covered with unit/integration tests (if applicable)
  • All automated tests passed successfully (all builds on Travis CI are green)

@coderimus
Copy link
Contributor Author

Hello dear @magento-engcom-team !
Can you please review this PR when you will have a time :) Just worry that it can be lost under the tons of newly pushed changes)
Looking forward to your response,
Alex

@dmanners
Copy link
Contributor

dmanners commented Jan 3, 2018

Hi @coderimus thank you for this pull request. We aim to get around this pull request during the next few days and will let you know if we need anything else from your side.

@coderimus
Copy link
Contributor Author

Hi @adrian-martinez-interactiv4 !
Thank you for taking this PR into the review. Sorry if I interrupt you but if it is possible can you please update me with the possible date when you will check it? :)
Best regards,
Alex

@adrian-martinez-interactiv4
Copy link
Contributor

Hi @coderimus , I'll check it tomorrow, and update you as soon as possible. I apologize for the delay.

@adrian-martinez-interactiv4
Copy link
Contributor

adrian-martinez-interactiv4 commented Jan 18, 2018

Hi again, @coderimus , I've trying to reproduce the issue with no luck, I've tried with cache enabled and disabled, indexes in Update On Save mode, even with config setting Display Out of Stock Products set to Yes and No values, always with the same result: when I update the simple product to remove its stock, it dissapears from product page; when I restore its stock, it shows up again.

I'm using attribute swatches, as marked in the testing scenario. Following is the process I've been doing, with the result, please check if I am missing something:

  • Initial status:
    captura de pantalla 2018-01-18 a las 19 57 55

captura de pantalla 2018-01-18 a las 19 58 40

captura de pantalla 2018-01-18 a las 19 59 16

  • Then, removing the stock for that product:
    captura de pantalla 2018-01-18 a las 20 00 48

  • Without erasing cache, only reloading product page, option disappears:
    captura de pantalla 2018-01-18 a las 20 02 08

I can't reproduce the issue, am I missing something?

@coderimus
Copy link
Contributor Author

@adrian-martinez-interactiv4 thank you for your time spent on this review and such detail description. I think that together we found one more aspect of this problem. This issue can be reproduced for a product having more than one item for options and options qty should be bigger than 1 and using swatches. In your example, you have 1 option with 2 products. If you would have 2 options with several products you could reproduce it.
I tested and reproduced this issue with the latest release 2.2 with sample data. Please, check screenshots and description:

  1. Choose a configurable product with 2 options with several products.
    image

  2. Choose any simple product, set it's qty to 0 and save. I did this without advanced inventory. Just put 0 into qty input and then press save. You will get status "Out of stock"
    image

  3. Go to the product page on the frontend and you will see that the option is still available and can be select.
    Screen Can you commit to repository a folder dev/tests/static ? #1 see black XS:
    image

Screen #2 check error:
image

My settings stock settings:
image
Index reindexed and cache flashed (just to be 100% sure)

Please, find time if it is suitable for you and try to reproduce it with my instructions.

Looking forward to your response,
Alex

@adrian-martinez-interactiv4 adrian-martinez-interactiv4 added the Reproduced on 2.2.x The issue has been reproduced on latest 2.2 release label Jan 19, 2018
@adrian-martinez-interactiv4
Copy link
Contributor

adrian-martinez-interactiv4 commented Jan 19, 2018

Hi again @coderimus , I've been able to reproduce the issue following your steps, applying your changes seems to solve the problem and mark the option as disabled in frontend, I'm starting a deeper review over the proposed changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderimus Please, review changes and let me know your thoughts, thank you!

* @return \Magento\Catalog\Model\ResourceModel\Product\Collection $collection
*/
public function addStockDataToCollection($collection, $isFilterInStock)
public function addStockDataToCollection($collection, $isFilterInStock = true, $websiteId = null)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to make already mandatory param $isFilterInStock optional, already existing calls to this methods include explicitly this flag. I think the idea is preset a default value for this field to simplify calls, but by now I would preserve this field as mandatory without default value, to force methods define flag value because this class is not marked as api and default value could change at any time, modifying the behaviour of classes using this method with the default value.

@@ -202,9 +202,10 @@ public function getProductCollection($lastEntityId = 0, $limit = 1000)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @return Status
*/
public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, \Magento\Store\Model\Website $website)
public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, $websiteId = null)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\Magento\Store\Model\Website $website => $websiteId = null.

  • There is no need to make already mandatory param $website optional, see other review comments for further explanation.
  • It's preferable to used typed parameters for methods, instead a generic variable. In this case, with this change, you are allowing to pass to this method as second parameter any kind of undesired variables or objects. New private method getWebsiteId also does not check $websiteId param type, so you could end with potential runtime errors when trying to build the query with the unknown-typed $websiteId param.

Please, consider reverting $websiteId param to its previous definition and adapt $websiteId = $this->getWebsiteId($websiteId); to $websiteId = $this->getWebsiteId($website->getId());, or use \Magento\Store\Model\StoreManager::getWebsite method to get a valid website from a websiteId.

*/
private function getWebsiteId($websiteId)
{
if ($websiteId === null) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it is not mandatory, please consider putting the operand that cannot be assigned on the left whenever you write a condition, to reduce possible errors.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependant of other review comments resolution.

private function getWebsiteId($websiteId)
{
if ($websiteId === null) {
$websiteId = $this->getStockConfiguration()->getDefaultScopeId();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using deprecated \Magento\CatalogInventory\Model\ResourceModel\Stock\Status::getStockConfiguration method; please consider injecting $stockConfiguration as optional param in the constructor, and instantiate it in the constructor if needed. Don't hesitate to ask if you need more info about this.

{
$collection->addAttributeToFilter(
ProductInterface::STATUS,
Status::STATUS_ENABLED
);

$stockFlag = 'has_stock_status_filter';
if (!$collection->hasFlag($stockFlag)) {
if (!$collection->hasFlag($stockFlag) || !$collection->getFlag($stockFlag)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no advantage in adding || !$collection->getFlag($stockFlag), since the value of this flag is irrelevant as long it is set, because its presence should be only used to mark the collection has already added the stock data to collection, to prevent query errors if \Magento\CatalogInventory\Model\ResourceModel\Stock\Status::addStockDataToCollection is called twice with the same collection before the collection is loaded. Also, this processor logic is very similar to
\Magento\CatalogInventory\Helper\Stock::addIsInStockFilterToCollection, if we preserved this here, we'd have to adapt also this method.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!$collection->hasFlag($stockFlag)) {

* @return Collection
*/
public function process(Collection $collection)
public function process(Collection $collection, $isFilterInStock = true)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, adapt \Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Collection\SalableProcessorTest to cover new parameter cases

if ($salableOnly) {
$collection = $this->salableProcessor->process($collection);
}
$collection->setFlag('has_stock_status_filter',false);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this line: $collection->setFlag('has_stock_status_filter',false);

$collection = $this->salableProcessor->process($collection);
}
$collection->setFlag('has_stock_status_filter',false);
$collection = $this->salableProcessor->process($collection, false);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$collection = $this->salableProcessor->process($collection, $salableOnly);

$this->salableProcessor
->expects($this->once())
->method('process')
->with($productCollection)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process() method is usually called with its second parameter as false, but test uses default method second param value, which value is true.
Change ->with($productCollection) with ->willReturnMap([ [$productCollection, false, $productCollection] ]);

->expects($this->once())
->method('process')
->with($productCollection)
->will($this->returnValue($productCollection));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

->will($this->returnValue($productCollection)): Remove this line after applying previous change, as return values are specified in map depending on method call params.

@coderimus
Copy link
Contributor Author

@adrian-martinez-interactiv4 thank you for this super detail and informative review. I will apply all requested changes and will follow them for all my new contributions! I will update PR with them tomorrow.
With great respect,
Alex

@coderimus
Copy link
Contributor Author

@adrian-martinez-interactiv4 sorry for the delay in reply. Had some blockers.
I implemented requested changes you reported in the review but some of them I left because without them explained problem still exists :( The core of this problem is in this method, as I can see: Magento\ConfigurableProduct\Model\Product\Type\Configurable::loadUsedProducts(\Magento\Catalog\Model\Product $product, $cacheKey, $salableOnly = false)

Previously, it was

    private function loadUsedProducts(\Magento\Catalog\Model\Product $product, $cacheKey, $salableOnly = false)
    {
        $dataFieldName = $salableOnly ? $this->usedSalableProducts : $this->_usedProducts;
        if (!$product->hasData($dataFieldName)) {
            $usedProducts = $this->readUsedProductsCacheData($cacheKey);
            if ($usedProducts === null) {
                $collection = $this->getConfiguredUsedProductCollection($product);
                if ($salableOnly) {
                    $collection = $this->salableProcessor->process($collection);
                }
                $usedProducts = array_values($collection->getItems());
                $this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey);
            }
            $product->setData($dataFieldName, $usedProducts);
        }

        return $product->getData($dataFieldName);
    }

as you can see the $collection = $this->salableProcessor->process($collection); would not be executed because the $salableOnly = false was always false due to the previous methods execution results.

I also did this $collection->setFlag('has_stock_status_filter',false); because without this value next code would not be executed:

    public function process(Collection $collection, $isFilterInStock = true)
    {
        $collection->addAttributeToFilter(
            ProductInterface::STATUS,
            Status::STATUS_ENABLED
        );

        $stockFlag = 'has_stock_status_filter';
        if (!$collection->hasFlag($stockFlag) || !$collection->getFlag($stockFlag)) {
            $stockStatusResource = $this->stockStatusFactory->create();
            $stockStatusResource->addStockDataToCollection($collection, $isFilterInStock);
            $collection->setFlag($stockFlag, true);
        }

        return $collection;
    }

I used this flag just to add stock status.

This issue is quite tricky in debugging and as a result difficult to explain all details briefly. Please, if you will have time for detail test, pull my changes and try to debug mentioned methods. Sorry if my description of changes is not so informative.

Thank you so much,
Alex

@adrian-martinez-interactiv4 adrian-martinez-interactiv4 force-pushed the bugfix/configurable-product-collection-salable-only-filter branch from f2abac5 to edf2dbf Compare January 25, 2018 12:05
@adrian-martinez-interactiv4
Copy link
Contributor

Hi @coderimus , I've been reviewing why proposed changes didn't work, and I think I have found the main problem with this issue. The method \Magento\ConfigurableProduct\Model\Product\Type\Configurable::getConfiguredUsedProductCollection method, used in \Magento\ConfigurableProduct\Model\Product\Type\Configurable::loadUsedProducts, forced to set flag has_stock_status_filter to true, what skipped stock status check. Now, stock status check is performed in frontend by \Magento\CatalogInventory\Model\AddStockStatusToCollection::beforeLoad method, that is a frontend plugin. Sorry I have added some commits to your branch, but as you said, this issue is quite tricky and suggest changes and explaining all modifications to make and what changes should be revertes is long, however, feel free to contact me on Slack for a detailed explanation about the proposed changes.

If you are comfortable with changes, you can squash commits and push force your branch so you can get full credit for this PR, as it belongs to you.

@adrian-martinez-interactiv4 adrian-martinez-interactiv4 changed the title FIX: out-of-stoock options for configurable product visible on frontend as sellable FIX: out-of-stock options for configurable product visible on frontend as sellable Jan 25, 2018
@coderimus coderimus force-pushed the bugfix/configurable-product-collection-salable-only-filter branch from edf2dbf to b349218 Compare January 25, 2018 13:49
@magento-engcom-team
Copy link
Contributor

@coderimus thank you for contributing. Please accept Community Contributors team invitation here to gain extended permissions for this repository.

@magento-team magento-team merged commit b349218 into magento:2.2-develop Jan 26, 2018
magento-team pushed a commit that referenced this pull request Jan 26, 2018
@adrian-martinez-interactiv4
Copy link
Contributor

@coderimus now that this is accepted and merged, would you consider to backport it to 2.3 if needed? Thank you in advance

@coderimus
Copy link
Contributor Author

@adrian-martinez-interactiv4 thank you! Sure, I will prepare the backport for this issue to 2.3 on Monday next week and will add you as a reviewer! :)

magento-engcom-team pushed a commit that referenced this pull request Feb 7, 2018
…rable product visible as sellable #13417

 - Merge Pull Request #13417 from Coderimus/magento2:backport/stock-status-filter-fix-for-configurable-products
 - Merged commits:
   1. d9111b0
magento-engcom-team pushed a commit that referenced this pull request Feb 7, 2018
magento-engcom-team added a commit that referenced this pull request Feb 7, 2018
Accepted Public Pull Requests:
 - #13251: Translate attribute label with default translation helper function (by @cmuench)
 - #12765: #12717 - Catalog Products List widget is not displayed on Storefront (by @RostislavS)
 - #13417: [Backport 2.3] #12936 out-of-stock options for configurable product visible as sellable (by @coderimus)


Fixed GitHub Issues:
 - #12717: Catalog Products List widget is not displayed on Storefront (reported by @alena-marchenko) has been fixed in #12765 by @RostislavS in 2.3-develop branch
   Related commits:
     1. d30696e
     2. 55a3ded
@duckchip
Copy link
Contributor

duckchip commented Apr 18, 2018

We're having a similar issue that is at least as important to fix but isn't solved by this patch.

In previous comments of @coderimus:
#12936 (comment)
"This issue can be reproduced for a product having more than one item for options and options qty should be bigger than 1 and using swatches. In your example, you have 1 option with 2 products. If you would have 2 options with several products you could reproduce it."

Though when 1 option type is used let's say 'size' with S, M, L, XL. And we make sure that option M has qty = 0 and is out of stock + the Inventory setting to display products out of stock is enabled, that option 'M' disappears from the source code like described in #12936 (comment) by @adrian-martinez-interactiv4

This can't be intended behaviour. We have to be able to see all the options AND make sure that the option that is out of stock either is crossed out or is hidden, but at least outputted in the source code which is not the case with this patch.

@scrivvles
Copy link

@duckchip, I am also having the same problem with 2.2.5. If I have a set of configurable products with simple attributes (like size), and some of them are out of stock, then the text swatches disappear. This happens regardless of my Store > Configuration > Catalog > Inventory being set to "Display Out of Stock Products : Yes"

Is this part of the same issue, or something different?

@josh-carter
Copy link

@duckchip @scrivvles We're currently having similar issues to you two.

I've taken a look and found where this stems from, it is indeed related to the PR for this issue, more specifically the code which gets the collection of products when getUsedProducts(...) is called:

$collection = $this->getConfiguredUsedProductCollection($product, false);

The false parameter results in the following function not setting the has_stock_status_filter on that collection:

   /**
     * Prepare collection for retrieving sub-products of specified configurable product
     *
     * Retrieve related products collection with additional configuration
     *
     * @param \Magento\Catalog\Model\Product $product
     * @param bool $skipStockFilter
     * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection
     */
    private function getConfiguredUsedProductCollection(
        \Magento\Catalog\Model\Product $product,
        $skipStockFilter = true
    ) {
        $collection = $this->getUsedProductCollection($product);

        if ($skipStockFilter) {
            $collection->setFlag('has_stock_status_filter', true);
        }

        $collection
            ->addAttributeToSelect($this->getAttributesForCollection($product))
            ->addFilterByRequiredOptions()
            ->setStoreId($product->getStoreId());

        $collection->addMediaGalleryData();
        $collection->addTierPriceData();

        return $collection;
    }

By not setting that flag, whenever the collection is processed via the Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor::process() method, out of stock products are then removed.

This happens because the aforementioned process method calls the addStockDataToCollection() method on the collection with the isFilterInStock parameter set as true:

$stockStatusResource->addStockDataToCollection($collection, true);

This results in a select query on the Collection where products stock status is in_stock:

        if ($isFilterInStock) {
            $collection->getSelect()->where(
                'stock_status_index.stock_status = ?',
                Stock\Status::STATUS_IN_STOCK
            );
        }

@scrivvles I too believe that we should be able to set Store > Configuration > Catalog > Inventory being set to "Display Out of Stock Products : Yes" and have our out of stock simple swatches and products showing on the frontend of the site as expected.

@dmanners @coderimus @adrian-martinez-interactiv4 Keen to hear your thoughts. It could pretty easily be fixed with a conditional to check whether the above configuration is set to No, if it is then within the SalableProcessor class pass the $isFilterInStock parameter as true, if not, don't pass the parameter as it has a default false value. This would prevent out of stock products being removed from any used Configurable child Product Collection and would be, in my opinion, rightly controlled via the configuration.

I'd be happy to create a PR for this.

@coderimus
Copy link
Contributor Author

@josh-carter thank you so much for this detailed review.
I just want to add that this issue looks a little bit more complicated and tricky as it could be.

As I understood, the main idea of the loadUsedProducts method is Load collection on sub-products for specified configurable product .

As you can find the getUsedProducts() method, which is the main entry point for the private loadUsedProducts, calls it without 3rd parameter - check here, please

    /**
     * Returns array of sub-products for specified configurable product
     *
     * Result array contains all children for specified configurable product
     *
     * @param  \Magento\Catalog\Model\Product $product
     * @param  array $requiredAttributeIds
     * @return ProductInterface[]
     */
    public function getUsedProducts($product, $requiredAttributeIds = null)
    {
        $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
        $keyParts = [
            __METHOD__,
            $product->getData($metadata->getLinkField()),
            $product->getStoreId(),
            $this->getCustomerSession()->getCustomerGroupId(),
            $requiredAttributeIds
        ];
        $cacheKey = $this->getUsedProductsCacheKey($keyParts);
        return $this->loadUsedProducts($product, $cacheKey);
    }

This 3rd parameter is a $salableOnly and it has next meaning according to the method description:

    /**
     * Load collection on sub-products for specified configurable product
     *
     * Load collection of sub-products, apply result to specified configurable product and store result to cache
     * Please note $salableOnly parameter is used for backwards compatibility because of deprecated method
     * getSalableUsedProducts
     * Number of loaded sub-products depends on $salableOnly parameter
     * $salableOnly = true - result array contains only salable sub-products
     * $salableOnly = false - result array contains all sub-products
     * $cacheKey - allow store result data in different cache records

So it means that this param should be always false and we have to have all sub products and if we will not have force false here $collection = $this->getConfiguredUsedProductCollection($product, false); we will have collection filtered by stock status.

Also, you can see that this method getSalableUsedProductCollections is also deprecated which means that by calling getUsedProducts we need to have all subproducts. But with one condition in mind described in the next paragraph.

And the main part here: the loadUsedProductCollection method is already sensitive to the stock showing setting. If you have "Display out of stock products" to Yes you will have collection consists of those methods. If setting eq to No only products which are in stock will be displayed.

Possible solution

From my point of view, the problem is in the Swatch module and how it operates with the received collection. Or the described problem is caused be another place in the code after the call to the getUsedProducts method. Need to be investigated.

From my point of view, we are not able to have any settings related condition because it will break the original flow.

@dmanners @adrian-martinez-interactiv4 please, when you will have time check this comment.

With a great respect,
Alex

@josh-carter
Copy link

@coderimus I think we need to move away from the getUsedProducts() collection for now at least. You're correct when you say that the described problem is caused be another place in the code after the call to the getUsedProducts method.

The problem is from loadUsedProducts() where it calls $collection = $this->getConfiguredUsedProductCollection($product, false); with that false parameter. It results in $collection->setFlag('has_stock_status_filter', true); not being set.

So this could still be seen as "not the problem" if i'm making sense, because really this is still fine, the collection doesn't have any stock_status_filter on it. But, following on from this, if the Configurable Parent Product ever has isSalable() called on it, it will lead to the SalableProcessor::process() method being called on the used collection, this can be seen in the Configurable Product Type Model:

    /**
     * Check is product available for sale
     *
     * @param \Magento\Catalog\Model\Product $product
     * @return bool
     */
    public function isSalable($product)
    {
        $salable = parent::isSalable($product);

        if ($salable !== false) {
            $collection = $this->getUsedProductCollection($product);
            $collection->addStoreFilter($this->getStoreFilter($product));
            $collection = $this->salableProcessor->process($collection);
            $salable = 0 !== $collection->getSize();
        }

        return $salable;
    }

Once the process() method is called:

    /**
     * Adds filters to the collection to help determine if product is available for sale.
     *
     * This method adds several additional checks for a children products availability.
     * Children products should be enabled and available in stock to be sold.
     * It also adds the specific flag to the collection to prevent the case
     * when filter already added and therefore may break the collection.
     *
     * @param Collection $collection
     * @return Collection
     */
    public function process(Collection $collection)
    {
        $collection->addAttributeToFilter(
            ProductInterface::STATUS,
            Status::STATUS_ENABLED
        );
        $stockFlag = 'has_stock_status_filter';
        if (!$collection->hasFlag($stockFlag)) {
            $stockStatusResource = $this->stockStatusFactory->create();
            $stockStatusResource->addStockDataToCollection($collection, true);
            $collection->setFlag($stockFlag, true);
        }
        return $collection;
    }

We see the check for the flag, which isn't set in the loadUsedProducts() method as we've clarified with the false parameter it passes when getting the collection. If it isn't set it calls addStockDataToCollection($collection, true) which then results in any products with stock_status set as out_of_stock as can be seen here:

        if ($isFilterInStock) {
            $collection->getSelect()->where(
                'stock_status_index.stock_status = ?',
                Stock\Status::STATUS_IN_STOCK
            );
        }

So is there actually a problem? And if so, where?

Personally i think there is a problem, or maybe a lack of clarity. When i read the configuration for Display out of Stock Products and set it to Yes - I read it as i've just made sure any out of stock products in my store will be displayed everywhere possible. Product Lists, Product View, Collections, Swatches.

For me I think there are a couple of options:

"Fix" SalableProcessor - My fix is in double quotes because some might not see it as a fix, it could be seen as working perfectly already. However, if we changed it to something similar to below:

    /**
     * Rewritten so that if show out of stock products is set to yes
     * it wont remove out of stock products
     *
     * @param Collection $collection
     * @return Collection
     */
    public function process(Collection $collection): Collection
    {
        $collection->addAttributeToFilter(
            ProductInterface::STATUS,
            Status::STATUS_ENABLED
        );

        $stockFlag = 'has_stock_status_filter';
        if (!$collection->hasFlag($stockFlag)) {
            $stockStatusResource = $this->stockStatusFactory->create();
            $isFilterInStock = $this->getIsFilterInStock();
            $stockStatusResource->addStockDataToCollection(
                $collection,
                $isFilterInStock
            );
            $collection->setFlag($stockFlag, true);
        }

        return $collection;
    }

    /**
     * Return Configuration value for showing Out Of Stock Products
     *
     * @return bool
     */
    private function getIsFilterInStock(): bool
    {
        $showOutOfStock = $this->stockConfiguration->isShowOutOfStock();
        return $showOutOfStock ? false : true;
    }

This is something i've personally implemented yesterday for a client following on from this issue.

Another option could be to add some more clarification to the configuration comment, just something simple saying that out of stock swatches/options wont be displayed.

Maybe theres a possibility that we could add a further configuration option somewhere to define some sort of further out of stock product display.

Conclusion

I'd say right now its pretty vague as to whether everything is working how it "should". Whether what we're talking about is actually a fix or whether its technically a feature. For now I'll wait to see what the others say.

cc: @dmanners @adrian-martinez-interactiv4

@josh-carter
Copy link

@dmanners @adrian-martinez-interactiv4 @coderimus Found this issue today which is related to our convo - #10454

@scrivvles
Copy link

For what it's worth, I agree with you completely @josh-carter This is a classic example of POLA - if everyone interprets something a certain way without any additional prompting or clarification, then that is the way it should work. (Or it should be modified).

I also would love to see your fix if you don't mind sharing it.

@josh-carter
Copy link

@scrivvles Linked it on that issue i've mentioned, the "fix" is at - https://github.com/interjar/configurable-child-visibility

But to be honest, i aim to get this fixed or at least cleared up in the core too so hopefully that fix extension will become redundant!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants