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

[Backport] Fix for #16069: Configurable product price is not displayed if all children are out of stock and even if Display Out of Stock Products is set to "yes" #18776 #18739

Closed
wants to merge 7 commits into from
15 changes: 14 additions & 1 deletion app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php
Expand Up @@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Catalog\Pricing\Render;

Expand Down Expand Up @@ -64,7 +65,9 @@ public function __construct(
*/
protected function _toHtml()
{
if (!$this->salableResolver->isSalable($this->getSaleableItem())) {
if ($this->isApplySalableCheck($this->getSaleableItem()) &&
!$this->salableResolver->isSalable($this->getSaleableItem())
) {
return '';
}

Expand All @@ -86,6 +89,16 @@ protected function _toHtml()
return $this->wrapResult($result);
}

/**
* @param SaleableInterface $salableItem
* @return bool
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function isApplySalableCheck(SaleableInterface $salableItem): bool
{
return true;
}

/**
* Check is MSRP applicable for the current product.
*
Expand Down
@@ -0,0 +1,29 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\ConfigurableProduct\Model\ResourceModel\Product;

use Magento\Framework\DB\Select;

/**
* Interface BaseSelectProcessorInterface
* @api
*/
interface BaseSelectProcessorInterface
{
/**
* Product table alias
*/
const PRODUCT_TABLE_ALIAS = 'child';

/**
* @param Select $select
* @param int $productId
* @return Select
*/
public function process(Select $select, int $productId): Select;
}
Expand Up @@ -3,9 +3,10 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\ConfigurableProduct\Model\ResourceModel\Product;

use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface;
use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface;

/**
Expand Down Expand Up @@ -39,14 +40,14 @@ public function __construct(
}

/**
* {@inheritdoc}
* @inheritdoc
*/
public function build($productId)
public function build($productId): array
{
$selects = $this->linkedProductSelectBuilder->build($productId);

foreach ($selects as $select) {
$this->baseSelectProcessor->process($select);
$this->baseSelectProcessor->process($select, (int)$productId);
}

return $selects;
Expand Down
Expand Up @@ -3,13 +3,17 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\ConfigurableProduct\Model\ResourceModel\Product;

use Magento\Framework\DB\Select;
use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface;
use Magento\Framework\App\ObjectManager;
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\CatalogInventory\Model\Stock\Status as StockStatus;
use Magento\CatalogInventory\Model\ResourceModel\Stock\Status as StockStatusResource;
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\StockStatusInterface
as StockStatusConfigurableInterface;

/**
* A Select object processor.
Expand All @@ -28,24 +32,35 @@ class StockStatusBaseSelectProcessor implements BaseSelectProcessorInterface
*/
private $stockStatusResource;

/**
* @var StockStatusConfigurableInterface
*/
private $stockConfigurable;

/**
* @param StockConfigurationInterface $stockConfig
* @param StockStatusResource $stockStatusResource
* @param StockStatusConfigurableInterface $stockConfigurable
*/
public function __construct(
StockConfigurationInterface $stockConfig,
StockStatusResource $stockStatusResource
StockStatusResource $stockStatusResource,
StockStatusConfigurableInterface $stockConfigurable = null
) {
$this->stockConfig = $stockConfig;
$this->stockStatusResource = $stockStatusResource;
$this->stockConfigurable = $stockConfigurable ?:
ObjectManager::getInstance()->get(StockStatusConfigurableInterface::class);
}

/**
* {@inheritdoc}
* @inheritdoc
*/
public function process(Select $select)
public function process(Select $select, int $productId): Select
{
if ($this->stockConfig->isShowOutOfStock()) {
if ($this->stockConfig->isShowOutOfStock() &&
!$this->isAllChildOutOfStock($productId)
) {
$select->joinInner(
['stock' => $this->stockStatusResource->getMainTable()],
sprintf(
Expand All @@ -61,4 +76,14 @@ public function process(Select $select)

return $select;
}

/**
* @param int $productId
* @return bool
* @throws \Exception
*/
private function isAllChildOutOfStock(int $productId): bool
{
return $this->stockConfigurable->isAllChildOutOfStock($productId);
}
}
@@ -0,0 +1,100 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable;

use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\App\ResourceConnection;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\CatalogInventory\Api\Data\StockStatusInterface as CatalogInventoryStockStatusInterface;
use Magento\Framework\DB\Adapter\AdapterInterface;

class StockStatus implements StockStatusInterface
{
/**
* @var ResourceConnection
*/
private $resource;

/**
* @var MetadataPool
*/
private $metadataPool;

/**
* StockStatus constructor.
* @param ResourceConnection $resource
* @param MetadataPool $metadataPool
*/
public function __construct(ResourceConnection $resource, MetadataPool $metadataPool)
{
$this->resource = $resource;
$this->metadataPool = $metadataPool;
}

/**
* @var array
*/
private $allChildOutOfStockInfo = [];

/**
* @inheritdoc
*/
public function isAllChildOutOfStock(int $productId): bool
{
if (isset($this->allChildOutOfStockInfo[$productId])) {
return $this->allChildOutOfStockInfo[$productId];
}

$statuses = $this->getAllChildStockInfo($productId);
$isAllChildOutOfStock = true;
foreach ($statuses as $status) {
if ($status === CatalogInventoryStockStatusInterface::STATUS_IN_STOCK) {
$isAllChildOutOfStock = false;
break;
}
}

$this->allChildOutOfStockInfo[$productId] = $isAllChildOutOfStock;

return $this->allChildOutOfStockInfo[$productId];
vpodorozh marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @return AdapterInterface
*/
protected function getConnection(): AdapterInterface
{
return $this->resource->getConnection();
}

/**
* @param int $productId
* @return array
* @throws \Exception
*/
private function getAllChildStockInfo(int $productId): array
{
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$productTable = $this->resource->getTableName('catalog_product_entity');
$productRelationTable = $this->resource->getTableName('catalog_product_relation');

$select = $this->getConnection()->select()
->from(['parent' => $productTable], '', [])
->joinInner(
['link' => $productRelationTable],
"link.parent_id = parent.$linkField",
['id' => 'child_id']
)->joinInner(
['stock' => $this->resource->getTableName('cataloginventory_stock_status')],
'stock.product_id = link.child_id',
['stock_status']
)->where(sprintf('parent.%s = ?', $linkField), $productId);

return $this->getConnection()->fetchPairs($select);
}
}
@@ -0,0 +1,22 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable;

/**
* Interface StockStatusInterface
* @api
*/
interface StockStatusInterface
{
/**
* @param int $productId
* @return bool
* @throws \Exception
*/
public function isAllChildOutOfStock(int $productId): bool;
}
Expand Up @@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\ConfigurableProduct\Pricing\Render;

use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface;
Expand All @@ -16,14 +18,24 @@
use Magento\Framework\Pricing\Render\RendererPool;
use Magento\Framework\Pricing\SaleableInterface;
use Magento\Framework\View\Element\Template\Context;
use Magento\CatalogInventory\Api\StockConfigurationInterface;

/**
* Class FinalPriceBox
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox
{
/**
* @var LowestPriceOptionsProviderInterface
*/
private $lowestPriceOptionsProvider;

/**
* @var StockConfigurationInterface
*/
private $stockConfig;

/**
* @param Context $context
* @param SaleableInterface $saleableItem
Expand All @@ -33,8 +45,10 @@ class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox
* @param array $data
* @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider
* @param SalableResolverInterface|null $salableResolver
* @param MinimalPriceCalculatorInterface|null $minimalPriceCalculator
* @param MinimalPriceCalculatorInterface|null $minPriceCalculator
* @param StockConfigurationInterface|null $stockConfig
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
Context $context,
Expand All @@ -45,7 +59,8 @@ public function __construct(
array $data = [],
LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider = null,
SalableResolverInterface $salableResolver = null,
MinimalPriceCalculatorInterface $minimalPriceCalculator = null
MinimalPriceCalculatorInterface $minPriceCalculator = null,
StockConfigurationInterface $stockConfig = null
) {
parent::__construct(
$context,
Expand All @@ -54,18 +69,20 @@ public function __construct(
$rendererPool,
$data,
$salableResolver,
$minimalPriceCalculator
$minPriceCalculator
);
$this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider ?:
ObjectManager::getInstance()->get(LowestPriceOptionsProviderInterface::class);
$this->stockConfig = $stockConfig ?:
ObjectManager::getInstance()->get(StockConfigurationInterface::class);
}

/**
* Define if the special price should be shown
*
* @return bool
*/
public function hasSpecialPrice()
public function hasSpecialPrice(): bool
{
$product = $this->getSaleableItem();
foreach ($this->lowestPriceOptionsProvider->getProducts($product) as $subProduct) {
Expand All @@ -77,4 +94,12 @@ public function hasSpecialPrice()
}
return false;
}

/**
* @inheritdoc
*/
protected function isApplySalableCheck(SaleableInterface $salableItem): bool
{
return !$this->stockConfig->isShowOutOfStock();
}
}