the reason you see 0 stock instead of "Out of stock" is because of inventory reservations. Stock is not actually deducted from saleable quantity until the order is shipped and complete. Hence, even when someone purchases all the quantity of product, the product will keep showing "In Stock" because its saleable check is still true.
You can disable the inventory reservation using Disable Stock Reservation
This should fix your issue. An alternate solution would be to add multiple checks on the front end to ensure that the product is displayed correctly. Something as follows:
app/code/Stackoverflow/ShowProductQuantity/etc/module.xml
<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Stackoverflow_ShowProductQuantity">
<sequence>
<module name="Magento_Catalog"/>
</sequence>
</module>
</config>
app/code/Stackoverflow/ShowProductQuantity/registration.php
<?php
/**
* Copyright © Magento All rights reserved.
* See COPYING.txt for license details.
*/
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Stackoverflow_ShowProductQuantity', __DIR__);
app/code/Stackoverflow/ShowProductQuantity/view/frontend/layout/catalog_product_view_type_simple.xml
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="product.info.simple" template="Stackoverflow_ShowProductQuantity::product/view/type/default.phtml">
<arguments>
<argument name="view_model" xsi:type="object">Stackoverflow\ShowProductQuantity\ViewModel\ProductStockInfo</argument>
</arguments>
</referenceBlock>
</body>
</page>
app/code/Stackoverflow/ShowProductQuantity/view/frontend/templates/product/view/type/default.phtml
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
use Magento\Catalog\Block\Product\View\AbstractView;
use Magento\Catalog\Model\Product;
use Stackoverflow\ShowProductQuantity\ViewModel\ProductStockInfo;
?>
<?php /* @var $block AbstractView */?>
<?php
/** @var Product $_product */
$_product = $block->getProduct();
/** @var ProductStockInfo|null $viewModel */
$viewModel = $block->getViewModel();
$productQty = $viewModel->getProductSaleableQty($_product);
?>
<?php if ($block->displayProductStockStatus()) :?>
<?php if ($_product->isAvailable() && !empty((int) $productQty)) :?>
<div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>">
<span><?= $block->escapeHtml(__('In stock: %1', $productQty)) ?></span>
</div>
<?php else :?>
<div class="stock unavailable" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>">
<span><?= $block->escapeHtml(__('Out of stock')) ?></span>
</div>
<?php endif; ?>
<?php endif; ?>
app/code/Stackoverflow/ShowProductQuantity/ViewModel/ProductStockInfo.php
<?php
declare(strict_types=1);
namespace Stackoverflow\ShowProductQuantity\ViewModel;
use Magento\Catalog\Model\Product;
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;
use Magento\InventorySalesApi\Api\StockResolverInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Bundle\Model\Product\Type as BundleProduct;
use Magento\GroupedProduct\Model\Product\Type\Grouped as GroupedProduct;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProduct;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\InventorySalesApi\Api\Data\SalesChannelInterface;
class ProductStockInfo implements ArgumentInterface
{
/**
* @var array
*/
private array $complexProductTypes = [
BundleProduct::TYPE_CODE,
GroupedProduct::TYPE_CODE,
ConfigurableProduct::TYPE_CODE
];
/**
* @var StoreManagerInterface
*/
private StoreManagerInterface $storeManager;
/**
* @var StockResolverInterface
*/
private StockResolverInterface $stockResolver;
/**
* @var GetProductSalableQtyInterface
*/
private GetProductSalableQtyInterface $productSaleableQty;
/**
* @param StoreManagerInterface $storeManager
* @param StockResolverInterface $stockResolver
* @param GetProductSalableQtyInterface $productSaleableQty
*/
public function __construct(
StoreManagerInterface $storeManager,
StockResolverInterface $stockResolver,
GetProductSalableQtyInterface $productSaleableQty
) {
$this->storeManager = $storeManager;
$this->stockResolver = $stockResolver;
$this->productSaleableQty = $productSaleableQty;
}
/**
* @param Product $product
* @return float|null
*/
public function getProductSaleableQty(Product $product): ?float
{
try {
$websiteCode = $this->storeManager->getWebsite()->getCode();
$stockDetails = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);
$stockId = $stockDetails->getStockId();
if (!in_array($product->getTypeId(), $this->complexProductTypes)) {
return $this->productSaleableQty->execute($product->getSku(), $stockId);
}
} catch (\Exception $ex) {
return null;
}
}
}
I would recommend disabling the stock reservation. You can also use the code that I shared as using the object manager directly is not recommended that's why I have used the view model here.
NOTE: Above code is only for showing the quantity for simple products on the PDP page as there are different layout handles for each type. The same view_model can be used to achieve the same for other types of products we just need to use the right type of layout handle and block.
Please let me know in case of any queries.