magento / magento2

Prior to making any Submission(s), you must sign an Adobe Contributor License Agreement, available here at: https://opensource.adobe.com/cla.html. All Submissions you make to Adobe Inc. and its affiliates, assigns and subsidiaries (collectively “Adobe”) are subject to the terms of the Adobe Contributor License Agreement.
http://www.magento.com
Open Software License 3.0
11.48k stars 9.29k forks source link

[2.4.4] Product with Salable Qty of 0 shows 'In Stock' on product page #35319

Closed FadedOut closed 2 years ago

FadedOut commented 2 years ago

FIX: A quick non-official hack/fix was posted further below by @loic-paquin and @jas8522 here

Preconditions (*)

  1. Upgraded from Magento 2.4.3-p1 to 2.4.4
  2. Default Luma Theme or 3rd Party
  3. Have backorders turned off (screenshot attached for confirmation that my settings are correct - though they did not change from 2.4.3-p1 where this was not a problem)

Steps to reproduce (*)

  1. Have a product with 0 salable quantity (configurable product)
  2. Go to frontend product page and check a variation/configuration (by selecting it)
  3. You can click "Add to Cart"

Expected result (*)

  1. It should say "Out of Stock" above SKU and/or you should not be able to select or click the variation swatch.
  2. Should not be able to click "Add to Cart" for an "Out of Stock" product (with 0 salable qty).

Actual result (*)

  1. It will allow you to click/select a swatch of an "out of stock" variation
  2. It will say "In Stock"
  3. Clicking "Add to Cart" it will reload page with error: "There are no source items with the in stock status"
  4. The "X in Stock" feature (only X left threshold) shows correctly when it is under my threshold, but does not show on a product that is really out of stock (with selecting the variation). So that function knows not to show because there are 0 in stock.
  5. Running re-index through CLI, I receive the following error (not sure if it's related - UPDATE: found this to not be related) and made no difference for this problem:

Product EAV index process error during indexation process: Deprecated Functionality: explode(): Passing null to parameter #2 ($string) of type string is deprecated in /home/********/public_html/vendor/magento/module-catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php on line 444

My previous installation of 2.4.3-p1 that I just upgraded from, this worked as intended (out of stock variations could not be selected/clicked. It showed a cross-through of the variation that was out of stock). It now does not do this, after upgrading to 2.4.4.

Below are screenshots of the product page as well as the admin/backend showing 0 for default stock quantity. This example is shown on the stock Magento Luma theme.

magento-244-frontend-instock-bug-product_view magento-244-frontend-instock-bug-backend_settings

EDIT/UPDATE: The original report of EAV indexing (using CLI) failing might be related, was incorrect. It is not related and made no difference for the bug of "Product with Salable Qty of 0 shows 'In Stock' on product page" - they are separate issues.

Additional Information: https://github.com/magento/magento2/issues/35319#issuecomment-1101209973


Please provide Severity assessment for the Issue as Reporter. This information will help during Confirmation and Issue triage processes.

Eikzx commented 5 months ago

Hello @nthurston I ran into the exact same issues using MAgento 2.4.6-p3. Have you found a fix for this back in the day? Greetings Eike.

stefanskotte commented 3 months ago

Hello @nthurston I ran into the exact same issues using MAgento 2.4.6-p3. Have you found a fix for this back in the day? Greetings Eike.

I still have this problem on 2.4.5-p7, even with 2.4-develop changes applied. I'm trying to figure out where this all goes wrong.

I have MSI enabled, with two different stock sources - both show incorrect on their respective website:

website 1: b2c stock website 2: b2b stock

Can believe this is so hard to get right.

jorgepires commented 2 months ago

I have managed to get this to work, my conclusion is that the isSalable() is not checking for any quantities on the salable stock. So if you make $product->isSalable() you always get true. This is not following best pratices, and is only used as a demonstration of the issue source.

My solution consists on only touching file vendor/magento/module-configurable-product/Helper/Data.php , the file below is from version 2.4.6-p4 , modified with the issue tag.

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\ConfigurableProduct\Helper;

use Magento\Catalog\Model\Product\Image\UrlBuilder;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Catalog\Helper\Image as ImageHelper;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Image;

use Magento\Catalog\Api\ProductRepositoryInterface; /* Salable Issue */
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface; /* Salable Issue */ 

/**
 * Class Data
 *
 * Helper class for getting options
 * @api
 * @since 100.0.2
 */
class Data
{
    /**
     * @var ImageHelper
     */
    protected $imageHelper;

    /**
     * @var UrlBuilder
     */
    private $imageUrlBuilder;

    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    protected $productRepository; /* Salable Issue */
    protected $getProductSalableQty; /* Salable Issue */

    /**
     * @param ImageHelper $imageHelper
     * @param UrlBuilder|null $urlBuilder
     * @param ScopeConfigInterface|null $scopeConfig
     */
    public function __construct(
        ImageHelper $imageHelper,
        UrlBuilder $urlBuilder = null,
        ?ScopeConfigInterface $scopeConfig = null,
        ProductRepositoryInterface $productRepository, /* Salable Issue */
        GetProductSalableQtyInterface $getProductSalableQty /* Salable Issue */
    ) {
        $this->imageHelper = $imageHelper;
        $this->imageUrlBuilder = $urlBuilder ?? ObjectManager::getInstance()->get(UrlBuilder::class);
        $this->scopeConfig = $scopeConfig ?? ObjectManager::getInstance()->get(ScopeConfigInterface::class);

    $this->productRepository = $productRepository; /* Salable Issue */
        $this->getProductSalableQty = $getProductSalableQty; /* Salable Issue */
    }

    /**
     * Retrieve collection of gallery images
     *
     * @param ProductInterface $product
     * @return Image[]|null
     */
    public function getGalleryImages(ProductInterface $product)
    {
        $images = $product->getMediaGalleryImages();
        if ($images instanceof \Magento\Framework\Data\Collection) {
            /** @var $image Image */
            foreach ($images as $image) {
                $smallImageUrl = $this->imageUrlBuilder
                    ->getUrl($image->getFile(), 'product_page_image_small');
                $image->setData('small_image_url', $smallImageUrl);

                $mediumImageUrl = $this->imageUrlBuilder
                    ->getUrl($image->getFile(), 'product_page_image_medium');
                $image->setData('medium_image_url', $mediumImageUrl);

                $largeImageUrl = $this->imageUrlBuilder
                    ->getUrl($image->getFile(), 'product_page_image_large');
                $image->setData('large_image_url', $largeImageUrl);
            }
        }

        return $images;
    }

    /**
     * Get Options for Configurable Product Options
     *
     * @param Product $currentProduct
     * @param array $allowedProducts
     * @return array
     */
    public function getOptions($currentProduct, $allowedProducts)
    {
        $options = [];
        $allowAttributes = $this->getAllowAttributes($currentProduct);

        foreach ($allowedProducts as $product) {
            $productId = $product->getId();
            foreach ($allowAttributes as $attribute) {
                $productAttribute = $attribute->getProductAttribute();
                $productAttributeId = $productAttribute->getId();
                $attributeValue = $product->getData($productAttribute->getAttributeCode());
                /*if ($this->canDisplayShowOutOfStockStatus()) {
                    if ($product->isSalable()) {
                        $options['salable'][$productAttributeId][$attributeValue][] = $productId;
                    }
                    $options[$productAttributeId][$attributeValue][] = $productId;
                } else {
                    if ($product->isSalable()) {
                        $options[$productAttributeId][$attributeValue][] = $productId;
                    }
                }*/

        /********************* Salable Issue ************************/
        $product = $this->productRepository->get($product->getSku());
        $stockItem = $product->getExtensionAttributes()->getStockItem();

        if( $stockItem->getManageStock() && $this->getProductSalableQty->execute($product->getSku(), 1) > 0) {
            $options['salable'][$productAttributeId][$attributeValue][] = $productId;
        } 

        if( !$stockItem->getManageStock() ) {
            $options['salable'][$productAttributeId][$attributeValue][] = $productId;
        }

        $options[$productAttributeId][$attributeValue][] = $productId;
        /********************* Salable Issue ************************/

                $options['index'][$productId][$productAttributeId] = $attributeValue;

            }
        }
        $options['canDisplayShowOutOfStockStatus'] = $this->canDisplayShowOutOfStockStatus();
        return $options;
    }

    /**
     * Get allowed attributes
     *
     * @param Product $product
     * @return array
     */
    public function getAllowAttributes($product)
    {
        return ($product->getTypeId() == Configurable::TYPE_CODE)
            ? $product->getTypeInstance()->getConfigurableAttributes($product)
            : [];
    }

    /**
     * Returns if display out of stock status set or not in catalog inventory
     *
     * @return bool
     */
    private function canDisplayShowOutOfStockStatus(): bool
    {
        return (bool) $this->scopeConfig->getValue('cataloginventory/options/show_out_of_stock');
    }

}
stefanskotte commented 2 months ago

I will try this asap, still have issues with configurables seemingly out of stock even though the simples have returned to a positive qty.