Skip to content

Commit de1df0b

Browse files
Merge remote-tracking branch 'magento-l3/ACP2E-1443' into PR-L3-21-02-2023
2 parents cedd3d2 + ea9695d commit de1df0b

File tree

10 files changed

+457
-303
lines changed

10 files changed

+457
-303
lines changed

app/code/Magento/Bundle/Model/Product/Type.php

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace Magento\Bundle\Model\Product;
88

99
use Magento\Bundle\Model\Option;
10+
use Magento\Bundle\Model\ResourceModel\Option\AreBundleOptionsSalable;
1011
use Magento\Bundle\Model\ResourceModel\Option\Collection;
1112
use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections;
1213
use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier;
@@ -170,6 +171,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType
170171
*/
171172
private $arrayUtility;
172173

174+
/**
175+
* @var AreBundleOptionsSalable
176+
*/
177+
private $areBundleOptionsSalable;
178+
173179
/**
174180
* @param \Magento\Catalog\Model\Product\Option $catalogProductOption
175181
* @param \Magento\Eav\Model\Config $eavConfig
@@ -196,7 +202,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType
196202
* @param MetadataPool|null $metadataPool
197203
* @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier
198204
* @param ArrayUtils|null $arrayUtility
199-
* @param UploaderFactory $uploaderFactory
205+
* @param UploaderFactory|null $uploaderFactory
206+
* @param AreBundleOptionsSalable|null $areBundleOptionsSalable
200207
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
201208
*/
202209
public function __construct(
@@ -225,7 +232,8 @@ public function __construct(
225232
MetadataPool $metadataPool = null,
226233
SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null,
227234
ArrayUtils $arrayUtility = null,
228-
UploaderFactory $uploaderFactory = null
235+
UploaderFactory $uploaderFactory = null,
236+
AreBundleOptionsSalable $areBundleOptionsSalable = null
229237
) {
230238
$this->_catalogProduct = $catalogProduct;
231239
$this->_catalogData = $catalogData;
@@ -246,6 +254,8 @@ public function __construct(
246254
$this->selectionCollectionFilterApplier = $selectionCollectionFilterApplier
247255
?: ObjectManager::getInstance()->get(SelectionCollectionFilterApplier::class);
248256
$this->arrayUtility= $arrayUtility ?: ObjectManager::getInstance()->get(ArrayUtils::class);
257+
$this->areBundleOptionsSalable = $areBundleOptionsSalable
258+
?? ObjectManager::getInstance()->get(AreBundleOptionsSalable::class);
249259

250260
parent::__construct(
251261
$catalogProductOption,
@@ -595,44 +605,8 @@ public function isSalable($product)
595605
return $product->getData('all_items_salable');
596606
}
597607

598-
$metadata = $this->metadataPool->getMetadata(
599-
\Magento\Catalog\Api\Data\ProductInterface::class
600-
);
601-
602-
$isSalable = false;
603-
foreach ($this->getOptionsCollection($product) as $option) {
604-
$hasSalable = false;
605-
606-
$selectionsCollection = $this->_bundleCollection->create();
607-
$selectionsCollection->addAttributeToSelect('status');
608-
$selectionsCollection->addQuantityFilter();
609-
$selectionsCollection->setFlag('product_children', true);
610-
$selectionsCollection->addFilterByRequiredOptions();
611-
$selectionsCollection->setOptionIdsFilter([$option->getId()]);
612-
613-
$this->selectionCollectionFilterApplier->apply(
614-
$selectionsCollection,
615-
'parent_product_id',
616-
$product->getData($metadata->getLinkField())
617-
);
618-
619-
foreach ($selectionsCollection as $selection) {
620-
if ($selection->isSalable()) {
621-
$hasSalable = true;
622-
break;
623-
}
624-
}
625-
626-
if ($hasSalable) {
627-
$isSalable = true;
628-
}
629-
630-
if (!$hasSalable && $option->getRequired()) {
631-
$isSalable = false;
632-
break;
633-
}
634-
}
635-
608+
$store = $this->_storeManager->getStore();
609+
$isSalable = $this->areBundleOptionsSalable->execute((int) $product->getEntityId(), (int) $store->getId());
636610
$product->setData('all_items_salable', $isSalable);
637611

638612
return $isSalable;

app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public function __construct(
4040
}
4141

4242
/**
43+
* Build bundle options select
44+
*
4345
* @param string $idxTable
4446
* @return Select
4547
*/
@@ -67,6 +69,10 @@ public function buildSelect($idxTable)
6769
['i' => $idxTable],
6870
'i.product_id = bs.product_id AND i.website_id = cis.website_id AND i.stock_id = cis.stock_id',
6971
[]
72+
)->joinLeft(
73+
['cisi' => $this->resourceConnection->getTableName('cataloginventory_stock_item')],
74+
'cisi.product_id = i.product_id AND cisi.stock_id = i.stock_id',
75+
[]
7076
)->joinLeft(
7177
['e' => $this->resourceConnection->getTableName('catalog_product_entity')],
7278
'e.entity_id = bs.product_id',

app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,7 @@ protected function _prepareBundleOptionStockData($entityIds = null, $usePrimaryT
9292
$idxTable = $usePrimaryTable ? $table : $this->getIdxTable();
9393
$select = $this->bundleOptionStockDataSelectBuilder->buildSelect($idxTable);
9494

95-
$status = new \Zend_Db_Expr(
96-
'MAX('
97-
. $connection->getCheckSql('e.required_options = 0', 'i.stock_status', '0')
98-
. ')'
99-
);
100-
95+
$status = $this->getOptionsStatusExpression();
10196
$select->columns(['status' => $status]);
10297

10398
if ($entityIds !== null) {
@@ -194,4 +189,49 @@ protected function _cleanBundleOptionStockData()
194189
$this->getConnection()->delete($this->_getBundleOptionTable());
195190
return $this;
196191
}
192+
193+
/**
194+
* Build expression for bundle options stock status
195+
*
196+
* @return \Zend_Db_Expr
197+
*/
198+
private function getOptionsStatusExpression(): \Zend_Db_Expr
199+
{
200+
$connection = $this->getConnection();
201+
$isAvailableExpr = $connection->getCheckSql(
202+
'bs.selection_can_change_qty = 0 AND bs.selection_qty > i.qty',
203+
'0',
204+
'i.stock_status'
205+
);
206+
if ($this->stockConfiguration->getBackorders()) {
207+
$backordersExpr = $connection->getCheckSql(
208+
'cisi.use_config_backorders = 0 AND cisi.backorders = 0',
209+
$isAvailableExpr,
210+
'i.stock_status'
211+
);
212+
} else {
213+
$backordersExpr = $connection->getCheckSql(
214+
'cisi.use_config_backorders = 0 AND cisi.backorders > 0',
215+
'i.stock_status',
216+
$isAvailableExpr
217+
);
218+
}
219+
if ($this->stockConfiguration->getManageStock()) {
220+
$statusExpr = $connection->getCheckSql(
221+
'cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 0',
222+
1,
223+
$backordersExpr
224+
);
225+
} else {
226+
$statusExpr = $connection->getCheckSql(
227+
'cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 1',
228+
$backordersExpr,
229+
1
230+
);
231+
}
232+
233+
return new \Zend_Db_Expr(
234+
'MAX(' . $connection->getCheckSql('e.required_options = 0', $statusExpr, '0') . ')'
235+
);
236+
}
197237
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Bundle\Model\ResourceModel\Option;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
12+
use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus;
13+
use Magento\Framework\App\ResourceConnection;
14+
use Magento\Framework\EntityManager\MetadataPool;
15+
16+
class AreBundleOptionsSalable
17+
{
18+
/**
19+
* @var ResourceConnection
20+
*/
21+
private $resourceConnection;
22+
23+
/**
24+
* @var MetadataPool
25+
*/
26+
private $metadataPool;
27+
28+
/**
29+
* @var ProductAttributeRepositoryInterface
30+
*/
31+
private $productAttributeRepository;
32+
33+
/**
34+
* @param ResourceConnection $resourceConnection
35+
* @param MetadataPool $metadataPool
36+
* @param ProductAttributeRepositoryInterface $productAttributeRepository
37+
*/
38+
public function __construct(
39+
ResourceConnection $resourceConnection,
40+
MetadataPool $metadataPool,
41+
ProductAttributeRepositoryInterface $productAttributeRepository
42+
) {
43+
$this->resourceConnection = $resourceConnection;
44+
$this->metadataPool = $metadataPool;
45+
$this->productAttributeRepository = $productAttributeRepository;
46+
}
47+
48+
/**
49+
* Check are bundle product options salable
50+
*
51+
* @param int $entityId
52+
* @param int $storeId
53+
* @return bool
54+
*/
55+
public function execute(int $entityId, int $storeId): bool
56+
{
57+
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
58+
$connection = $this->resourceConnection->getConnection();
59+
$optionsSaleabilitySelect = $connection->select()
60+
->from(
61+
['parent_products' => $this->resourceConnection->getTableName('catalog_product_entity')],
62+
[]
63+
)->joinInner(
64+
['bundle_options' => $this->resourceConnection->getTableName('catalog_product_bundle_option')],
65+
"bundle_options.parent_id = parent_products.{$linkField}",
66+
[]
67+
)->joinInner(
68+
['bundle_selections' => $this->resourceConnection->getTableName('catalog_product_bundle_selection')],
69+
'bundle_selections.option_id = bundle_options.option_id',
70+
[]
71+
)->joinInner(
72+
['child_products' => $this->resourceConnection->getTableName('catalog_product_entity')],
73+
'child_products.entity_id = bundle_selections.product_id',
74+
[]
75+
)->group(
76+
['bundle_options.parent_id', 'bundle_options.option_id']
77+
)->where(
78+
'parent_products.entity_id = ?',
79+
$entityId
80+
);
81+
$statusAttr = $this->productAttributeRepository->get(ProductInterface::STATUS);
82+
$optionsSaleabilitySelect->joinInner(
83+
['child_status_global' => $statusAttr->getBackendTable()],
84+
"child_status_global.{$linkField} = child_products.{$linkField}"
85+
. " AND child_status_global.attribute_id = {$statusAttr->getAttributeId()}"
86+
. " AND child_status_global.store_id = 0",
87+
[]
88+
)->joinLeft(
89+
['child_status_store' => $statusAttr->getBackendTable()],
90+
"child_status_store.{$linkField} = child_products.{$linkField}"
91+
. " AND child_status_store.attribute_id = {$statusAttr->getAttributeId()}"
92+
. " AND child_status_store.store_id = {$storeId}",
93+
[]
94+
);
95+
$isOptionSalableExpr = new \Zend_Db_Expr(
96+
sprintf(
97+
'MAX(IFNULL(child_status_store.value, child_status_global.value) != %s)',
98+
ProductStatus::STATUS_DISABLED
99+
)
100+
);
101+
$isRequiredOptionUnsalable = $connection->getCheckSql(
102+
'required = 1 AND ' . $isOptionSalableExpr . ' = 0',
103+
'1',
104+
'0'
105+
);
106+
$optionsSaleabilitySelect->columns([
107+
'required' => 'bundle_options.required',
108+
'is_salable' => $isOptionSalableExpr,
109+
'is_required_and_unsalable' => $isRequiredOptionUnsalable,
110+
]);
111+
112+
$select = $connection->select()->from(
113+
$optionsSaleabilitySelect,
114+
[new \Zend_Db_Expr('(MAX(is_salable) = 1 AND MAX(is_required_and_unsalable) = 0)')]
115+
);
116+
$isSalable = $connection->fetchOne($select);
117+
118+
return (bool) $isSalable;
119+
}
120+
}

0 commit comments

Comments
 (0)