Skip to content

Commit 6b4b959

Browse files
committed
MAGETWO-86218: Enable Add to Cart on bundle products when bundle item qty is not User Defined while backorders are allowed
1 parent 0a52f88 commit 6b4b959

File tree

3 files changed

+282
-23
lines changed

3 files changed

+282
-23
lines changed

app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
*/
66
namespace Magento\Bundle\Model\ResourceModel\Selection;
77

8-
use Magento\Customer\Api\GroupManagementInterface;
98
use Magento\Framework\DataObject;
109
use Magento\Framework\DB\Select;
11-
use Magento\Framework\EntityManager\MetadataPool;
1210
use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
1311
use Magento\Framework\App\ObjectManager;
1412

@@ -45,6 +43,95 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
4543
*/
4644
private $websiteScopePriceJoined = false;
4745

46+
/**
47+
* @var \Magento\CatalogInventory\Model\ResourceModel\Stock\Item
48+
*/
49+
private $stockItem;
50+
51+
/**
52+
* Collection constructor.
53+
* @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
54+
* @param \Psr\Log\LoggerInterface $logger
55+
* @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
56+
* @param \Magento\Framework\Event\ManagerInterface $eventManager
57+
* @param \Magento\Eav\Model\Config $eavConfig
58+
* @param \Magento\Framework\App\ResourceConnection $resource
59+
* @param \Magento\Eav\Model\EntityFactory $eavEntityFactory
60+
* @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper
61+
* @param \Magento\Framework\Validator\UniversalFactory $universalFactory
62+
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
63+
* @param \Magento\Framework\Module\Manager $moduleManager
64+
* @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState
65+
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
66+
* @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory
67+
* @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl
68+
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
69+
* @param \Magento\Customer\Model\Session $customerSession
70+
* @param \Magento\Framework\Stdlib\DateTime $dateTime
71+
* @param \Magento\Customer\Api\GroupManagementInterface $groupManagement
72+
* @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection
73+
* @param ProductLimitationFactory|null $productLimitationFactory
74+
* @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool
75+
* @param \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer|null $tableMaintainer
76+
* @param \Magento\CatalogInventory\Model\ResourceModel\Stock\Item|null $stockItem
77+
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
78+
*/
79+
public function __construct(
80+
\Magento\Framework\Data\Collection\EntityFactory $entityFactory,
81+
\Psr\Log\LoggerInterface $logger,
82+
\Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
83+
\Magento\Framework\Event\ManagerInterface $eventManager,
84+
\Magento\Eav\Model\Config $eavConfig,
85+
\Magento\Framework\App\ResourceConnection $resource,
86+
\Magento\Eav\Model\EntityFactory $eavEntityFactory,
87+
\Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
88+
\Magento\Framework\Validator\UniversalFactory $universalFactory,
89+
\Magento\Store\Model\StoreManagerInterface $storeManager,
90+
\Magento\Framework\Module\Manager $moduleManager,
91+
\Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState,
92+
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
93+
\Magento\Catalog\Model\Product\OptionFactory $productOptionFactory,
94+
\Magento\Catalog\Model\ResourceModel\Url $catalogUrl,
95+
\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
96+
\Magento\Customer\Model\Session $customerSession,
97+
\Magento\Framework\Stdlib\DateTime $dateTime,
98+
\Magento\Customer\Api\GroupManagementInterface $groupManagement,
99+
\Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
100+
ProductLimitationFactory $productLimitationFactory = null,
101+
\Magento\Framework\EntityManager\MetadataPool $metadataPool = null,
102+
\Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer $tableMaintainer = null,
103+
\Magento\CatalogInventory\Model\ResourceModel\Stock\Item $stockItem = null
104+
) {
105+
parent::__construct(
106+
$entityFactory,
107+
$logger,
108+
$fetchStrategy,
109+
$eventManager,
110+
$eavConfig,
111+
$resource,
112+
$eavEntityFactory,
113+
$resourceHelper,
114+
$universalFactory,
115+
$storeManager,
116+
$moduleManager,
117+
$catalogProductFlatState,
118+
$scopeConfig,
119+
$productOptionFactory,
120+
$catalogUrl,
121+
$localeDate,
122+
$customerSession,
123+
$dateTime,
124+
$groupManagement,
125+
$connection,
126+
$productLimitationFactory,
127+
$metadataPool,
128+
$tableMaintainer
129+
);
130+
131+
$this->stockItem = $stockItem
132+
?? ObjectManager::getInstance()->get(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class);
133+
}
134+
48135
/**
49136
* Initialize collection
50137
*
@@ -170,27 +257,31 @@ public function setPositionOrder()
170257
*/
171258
public function addQuantityFilter()
172259
{
173-
$stockItemTable = $this->getTable('cataloginventory_stock_item');
174-
$stockStatusTable = $this->getTable('cataloginventory_stock_status');
260+
$manageStockExpr = $this->stockItem->getManageStockExpr('stock_item');
261+
$backordersExpr = $this->stockItem->getBackordersExpr('stock_item');
262+
$minQtyExpr = $this->getConnection()->getCheckSql(
263+
'selection.selection_can_change_qty',
264+
$this->stockItem->getMinSaleQtyExpr('stock_item'),
265+
'selection.selection_qty'
266+
);
267+
268+
$where = $manageStockExpr . ' = 0';
269+
$where .= ' OR ('
270+
. 'stock_item.is_in_stock = ' . \Magento\CatalogInventory\Model\Stock::STOCK_IN_STOCK
271+
. ' AND ('
272+
. $backordersExpr . ' != ' . \Magento\CatalogInventory\Model\Stock::BACKORDERS_NO
273+
. ' OR '
274+
. $minQtyExpr . ' <= stock_item.qty'
275+
. ')'
276+
. ')';
277+
175278
$this->getSelect()
176279
->joinInner(
177-
['stock' => $stockStatusTable],
178-
'selection.product_id = stock.product_id',
179-
[]
180-
)->joinInner(
181-
['stock_item' => $stockItemTable],
280+
['stock_item' => $this->stockItem->getMainTable()],
182281
'selection.product_id = stock_item.product_id',
183282
[]
184-
)
185-
->where(
186-
'('
187-
. 'selection.selection_can_change_qty'
188-
. ' or '
189-
. 'selection.selection_qty <= stock.qty'
190-
. ' or '
191-
.'stock_item.manage_stock = 0'
192-
. ') and stock.stock_status = 1'
193-
);
283+
)->where($where);
284+
194285
return $this;
195286
}
196287

app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,48 @@ public function updateLowStockDate(int $websiteId)
263263
$connection->update($this->getMainTable(), $value, $where);
264264
}
265265

266+
public function getManageStockExpr(string $tableAlias = ''): \Zend_Db_Expr
267+
{
268+
if ($tableAlias) {
269+
$tableAlias .= '.';
270+
}
271+
$manageStock = $this->getConnection()->getCheckSql(
272+
$tableAlias . 'use_config_manage_stock = 1',
273+
$this->stockConfiguration->getManageStock(),
274+
$tableAlias . 'manage_stock'
275+
);
276+
277+
return $manageStock;
278+
}
279+
280+
public function getBackordersExpr(string $tableAlias = ''): \Zend_Db_Expr
281+
{
282+
if ($tableAlias) {
283+
$tableAlias .= '.';
284+
}
285+
$itemBackorders = $this->getConnection()->getCheckSql(
286+
$tableAlias . 'use_config_backorders = 1',
287+
$this->stockConfiguration->getBackorders(),
288+
$tableAlias . 'backorders'
289+
);
290+
291+
return $itemBackorders;
292+
}
293+
294+
public function getMinSaleQtyExpr(string $tableAlias = ''): \Zend_Db_Expr
295+
{
296+
if ($tableAlias) {
297+
$tableAlias .= '.';
298+
}
299+
$itemMinSaleQty = $this->getConnection()->getCheckSql(
300+
$tableAlias . 'use_config_min_sale_qty = 1',
301+
$this->stockConfiguration->getMinSaleQty(),
302+
$tableAlias . 'min_sale_qty'
303+
);
304+
305+
return $itemMinSaleQty;
306+
}
307+
266308
/**
267309
* Build select for products with types from config
268310
*

dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Magento\Catalog\Model\Product\Attribute\Source\Status;
1818
use Magento\Catalog\Model\Product\Type;
1919
use Magento\Catalog\Model\Product\Visibility;
20+
use Magento\CatalogInventory\Api\StockRegistryInterface;
21+
use Magento\CatalogInventory\Model\Stock;
2022
use Magento\Framework\ObjectManagerInterface;
2123
use Magento\Store\Api\StoreRepositoryInterface;
2224
use Magento\Store\Model\StoreManagerInterface;
@@ -35,9 +37,15 @@ class ProductTest extends \PHPUnit\Framework\TestCase
3537
*/
3638
private $objectManager;
3739

40+
/**
41+
* @var ProductRepositoryInterface
42+
*/
43+
private $productRepository;
44+
3845
protected function setUp()
3946
{
4047
$this->objectManager = Bootstrap::getObjectManager();
48+
$this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
4149

4250
$this->model = $this->objectManager->create(Product::class);
4351
$this->model->setTypeId(Type::TYPE_BUNDLE);
@@ -104,9 +112,7 @@ public function testIsComposite()
104112
*/
105113
public function testMultipleStores()
106114
{
107-
/** @var ProductRepositoryInterface $productRepository */
108-
$productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
109-
$bundle = $productRepository->get('bundle-product');
115+
$bundle = $this->productRepository->get('bundle-product');
110116

111117
/** @var StoreRepositoryInterface $storeRepository */
112118
$storeRepository = $this->objectManager->get(StoreRepositoryInterface::class);
@@ -120,8 +126,128 @@ public function testMultipleStores()
120126

121127
$bundle->setStoreId($store->getId())
122128
->setCopyFromView(true);
123-
$updatedBundle = $productRepository->save($bundle);
129+
$updatedBundle = $this->productRepository->save($bundle);
124130

125131
self::assertEquals($store->getId(), $updatedBundle->getStoreId());
126132
}
133+
134+
/**
135+
* @param float $selectionQty
136+
* @param float $qty
137+
* @param int $isInStock
138+
* @param bool $manageStock
139+
* @param int $backorders
140+
* @param bool $isSalable
141+
* @magentoAppIsolation enabled
142+
* @magentoDataFixture Magento/Bundle/_files/product.php
143+
* @dataProvider stockConfigDataProvider
144+
* @covers \Magento\Catalog\Model\Product::isSalable
145+
*/
146+
public function testIsSalable(
147+
float $selectionQty,
148+
float $qty,
149+
int $isInStock,
150+
bool $manageStock,
151+
int $backorders,
152+
bool $isSalable
153+
) {
154+
/** @var \Magento\Catalog\Model\Product $bundle */
155+
$bundle = $this->productRepository->get('bundle-product');
156+
157+
$child = $this->productRepository->get('simple');
158+
$stockRegistry = $this->objectManager->get(StockRegistryInterface::class);
159+
$childStockItem = $stockRegistry->getStockItem($child->getId());
160+
$childStockItem->setQty($qty);
161+
$childStockItem->setIsInStock($isInStock);
162+
$childStockItem->setUseConfigManageStock(false);
163+
$childStockItem->setManageStock($manageStock);
164+
$childStockItem->setUseConfigBackorders(false);
165+
$childStockItem->setBackorders($backorders);
166+
$stockRegistry->updateStockItemBySku($child->getSku(), $childStockItem);
167+
168+
$linkManagement = $this->objectManager->get(\Magento\Bundle\Api\ProductLinkManagementInterface::class);
169+
foreach ($bundle->getExtensionAttributes()->getBundleProductOptions() as $productOption) {
170+
foreach ($productOption->getProductLinks() as $productLink) {
171+
$productLink->setCanChangeQuantity(0);
172+
$productLink->setQty($selectionQty);
173+
$linkManagement->saveChild($bundle->getSku(), $productLink);
174+
175+
}
176+
}
177+
178+
$this->assertEquals($isSalable, $bundle->isSalable());
179+
}
180+
181+
/**
182+
* @return array
183+
*/
184+
public function stockConfigDataProvider(): array
185+
{
186+
$qtyVars = [0, 10];
187+
$isInStockVars = [
188+
Stock::STOCK_OUT_OF_STOCK,
189+
Stock::STOCK_IN_STOCK,
190+
];
191+
$manageStockVars = [false, true];
192+
$backordersVars = [
193+
Stock::BACKORDERS_NO,
194+
Stock::BACKORDERS_YES_NONOTIFY,
195+
Stock::BACKORDERS_YES_NOTIFY,
196+
];
197+
$selectionQtyVars = [5, 10, 15];
198+
199+
$variations = [];
200+
foreach ($qtyVars as $qty) {
201+
foreach ($isInStockVars as $isInStock) {
202+
foreach ($manageStockVars as $manageStock) {
203+
foreach ($backordersVars as $backorders) {
204+
foreach ($selectionQtyVars as $selectionQty) {
205+
$variationName = "selectionQty: {$selectionQty}"
206+
. " qty: {$qty}"
207+
. " isInStock: {$isInStock}"
208+
. " manageStock: {$manageStock}"
209+
. " backorders: {$backorders}";
210+
$isSalable = $this->checkIsSalable(
211+
$selectionQty,
212+
$qty,
213+
$isInStock,
214+
$manageStock,
215+
$backorders
216+
);
217+
218+
$variations[$variationName] = [
219+
$selectionQty,
220+
$qty,
221+
$isInStock,
222+
$manageStock,
223+
$backorders,
224+
$isSalable
225+
];
226+
}
227+
}
228+
}
229+
}
230+
}
231+
232+
return $variations;
233+
}
234+
235+
/**
236+
* @param float $selectionQty
237+
* @param float $qty
238+
* @param int $isInStock
239+
* @param bool $manageStock
240+
* @param int $backorders
241+
* @return bool
242+
* @see \Magento\Bundle\Model\ResourceModel\Selection\Collection::addQuantityFilter
243+
*/
244+
private function checkIsSalable(
245+
float $selectionQty,
246+
float $qty,
247+
int $isInStock,
248+
bool $manageStock,
249+
int $backorders
250+
): bool {
251+
return !$manageStock || ($isInStock && ($backorders || $selectionQty <= $qty));
252+
}
127253
}

0 commit comments

Comments
 (0)