Skip to content

Commit 53d0dab

Browse files
Merge pull request #1897 from magento-qwerty/2.1-PR-20171221
Fixed issues: - MAGETWO-59163: Category product count incorporating products with visibility set to search only - MAGETWO-69701: [GitHub] Concurrent checkouts can lead to negative stock #6363 [backport 2.1]
2 parents e738316 + 6d56e1e commit 53d0dab

File tree

14 files changed

+265
-68
lines changed

14 files changed

+265
-68
lines changed

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
3+
* Copyright © 2013-2018 Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
66

@@ -126,7 +126,7 @@ public function lockProductsStock($productIds, $websiteId)
126126
}
127127
$itemTable = $this->getTable('cataloginventory_stock_item');
128128
$select = $this->getConnection()->select()->from(['si' => $itemTable])
129-
->where('website_id=?', $websiteId)
129+
->where('website_id = ?', $websiteId)
130130
->where('product_id IN(?)', $productIds)
131131
->forUpdate(true);
132132

@@ -139,9 +139,15 @@ public function lockProductsStock($productIds, $websiteId)
139139
'type_id' => 'type_id'
140140
]
141141
);
142-
$this->getConnection()->query($select);
142+
$items = [];
143143

144-
return $this->getConnection()->fetchAll($selectProducts);
144+
foreach ($this->getConnection()->query($select)->fetchAll() as $si) {
145+
$items[$si['product_id']] = $si;
146+
}
147+
foreach ($this->getConnection()->fetchAll($selectProducts) as $p) {
148+
$items[$p['product_id']]['type_id'] = $p['type_id'];
149+
}
150+
return $items;
145151
}
146152

147153
/**

app/code/Magento/CatalogInventory/Model/StockManagement.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
3+
* Copyright © 2013-2018 Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
66
namespace Magento\CatalogInventory\Model;
@@ -48,28 +48,37 @@ class StockManagement implements StockManagementInterface
4848
*/
4949
private $qtyCounter;
5050

51+
/**
52+
* @var StockRegistryStorage
53+
*/
54+
private $stockRegistryStorage;
55+
5156
/**
5257
* @param ResourceStock $stockResource
5358
* @param StockRegistryProviderInterface $stockRegistryProvider
5459
* @param StockState $stockState
5560
* @param StockConfigurationInterface $stockConfiguration
5661
* @param ProductRepositoryInterface $productRepository
5762
* @param QtyCounterInterface $qtyCounter
63+
* @param StockRegistryStorage|null $stockRegistryStorage
5864
*/
5965
public function __construct(
6066
ResourceStock $stockResource,
6167
StockRegistryProviderInterface $stockRegistryProvider,
6268
StockState $stockState,
6369
StockConfigurationInterface $stockConfiguration,
6470
ProductRepositoryInterface $productRepository,
65-
QtyCounterInterface $qtyCounter
71+
QtyCounterInterface $qtyCounter,
72+
StockRegistryStorage $stockRegistryStorage = null
6673
) {
6774
$this->stockRegistryProvider = $stockRegistryProvider;
6875
$this->stockState = $stockState;
6976
$this->stockConfiguration = $stockConfiguration;
7077
$this->productRepository = $productRepository;
7178
$this->qtyCounter = $qtyCounter;
7279
$this->resource = $stockResource;
80+
$this->stockRegistryStorage = $stockRegistryStorage ?: \Magento\Framework\App\ObjectManager::getInstance()
81+
->get(StockRegistryStorage::class);
7382
}
7483

7584
/**
@@ -92,9 +101,12 @@ public function registerProductsSale($items, $websiteId = null)
92101
$fullSaveItems = $registeredItems = [];
93102
foreach ($lockedItems as $lockedItemRecord) {
94103
$productId = $lockedItemRecord['product_id'];
104+
$this->stockRegistryStorage->removeStockItem($productId, $websiteId);
105+
95106
/** @var StockItemInterface $stockItem */
96107
$orderedQty = $items[$productId];
97108
$stockItem = $this->stockRegistryProvider->getStockItem($productId, $websiteId);
109+
$stockItem->setQty($lockedItemRecord['qty']); // update data from locked item
98110
$canSubtractQty = $stockItem->getItemId() && $this->canSubtractQty($stockItem);
99111
if (!$canSubtractQty || !$this->stockConfiguration->isQty($lockedItemRecord['type_id'])) {
100112
continue;
@@ -180,7 +192,7 @@ protected function getProductType($productId)
180192
}
181193

182194
/**
183-
* @return Stock
195+
* @return ResourceStock
184196
*/
185197
protected function getResource()
186198
{

app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
3+
* Copyright © 2013-2018 Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
66
namespace Magento\CatalogInventory\Test\Unit\Model\ResourceModel;
@@ -67,6 +67,11 @@ class StockTest extends \PHPUnit_Framework_TestCase
6767
*/
6868
protected $selectMock;
6969

70+
/**
71+
* @var \PHPUnit_Framework_MockObject_MockObject|\Zend_Db_Statement_Interface
72+
*/
73+
protected $statementMock;
74+
7075
/**
7176
* Prepare subjects for tests.
7277
*
@@ -95,6 +100,7 @@ protected function setUp()
95100
$this->connectionMock = $this->getMockBuilder(Mysql::class)
96101
->disableOriginalConstructor()
97102
->getMock();
103+
$this->statementMock = $this->getMockForAbstractClass(\Zend_Db_Statement_Interface::class);
98104
$this->stock = $this->getMockBuilder(Stock::class)
99105
->setMethods(['getTable', 'getConnection'])
100106
->setConstructorArgs(
@@ -119,7 +125,21 @@ public function testLockProductsStock()
119125
{
120126
$websiteId = 0;
121127
$productIds = [1, 2, 3];
122-
$result = ['testResult'];
128+
$result = [
129+
1 => [
130+
'product_id' => 1,
131+
'type_id' => 'simple'
132+
],
133+
2 => [
134+
'product_id' => 2,
135+
'type_id' => 'simple'
136+
],
137+
3 => [
138+
'product_id' => 3,
139+
'type_id' => 'simple'
140+
]
141+
];
142+
123143
$this->selectMock->expects(self::exactly(2))
124144
->method('from')
125145
->withConsecutive(
@@ -130,7 +150,7 @@ public function testLockProductsStock()
130150
$this->selectMock->expects(self::exactly(3))
131151
->method('where')
132152
->withConsecutive(
133-
[self::identicalTo('website_id=?'), self::identicalTo($websiteId)],
153+
[self::identicalTo('website_id = ?'), self::identicalTo($websiteId)],
134154
[self::identicalTo('product_id IN(?)'), self::identicalTo($productIds)],
135155
[self::identicalTo('entity_id IN (?)'), self::identicalTo($productIds)]
136156
)
@@ -149,10 +169,19 @@ public function testLockProductsStock()
149169
->willReturn($this->selectMock);
150170
$this->connectionMock->expects(self::once())
151171
->method('query')
152-
->with(self::identicalTo($this->selectMock));
172+
->with(self::identicalTo($this->selectMock))
173+
->willReturn($this->statementMock);
174+
$this->statementMock->expects(self::once())
175+
->method('fetchAll')
176+
->willReturn([
177+
1 => ['product_id' => 1],
178+
2 => ['product_id' => 2],
179+
3 => ['product_id' => 3]
180+
]);
181+
153182
$this->connectionMock->expects(self::once())
154183
->method('fetchAll')
155-
->with($this->selectMock)
184+
->with(self::identicalTo($this->selectMock))
156185
->willReturn($result);
157186

158187
$this->stock->expects(self::exactly(2))

app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
3+
* Copyright © 2013-2018 Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
66

@@ -35,7 +35,8 @@ public function getAlias(\Magento\Framework\Search\Request\FilterInterface $filt
3535
$alias = 'price_index';
3636
break;
3737
case 'category_ids':
38-
$alias = 'category_ids_index';
38+
case 'visibility':
39+
$alias = 'category_products_index';
3940
break;
4041
default:
4142
$alias = $field . RequestGenerator::FILTER_SUFFIX;

app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
3+
* Copyright © 2013-2018 Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
66
namespace Magento\CatalogSearch\Model\Adapter\Mysql\Filter;
@@ -141,7 +141,10 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu
141141
$query
142142
);
143143
} elseif ($filter->getField() === 'category_ids') {
144-
return 'category_ids_index.category_id = ' . (int) $filter->getValue();
144+
return "{$this->aliasResolver->getAlias($filter)}.category_id = "
145+
. (int) $filter->getValue();
146+
} elseif ($filter->getField() === 'visibility') {
147+
return "{$this->aliasResolver->getAlias($filter)}." . $query;
145148
} elseif ($attribute->isStatic()) {
146149
$alias = $this->aliasResolver->getAlias($filter);
147150
$resultQuery = str_replace(

app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
3+
* Copyright © 2013-2018 Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
66
namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext;
@@ -254,7 +254,7 @@ public function setFilterBuilder(\Magento\Framework\Api\FilterBuilder $object)
254254
* Apply attribute filter to facet collection
255255
*
256256
* @param string $field
257-
* @param null $condition
257+
* @param null|string|array $condition
258258
* @return $this
259259
*/
260260
public function addFieldToFilter($field, $condition = null)
@@ -265,22 +265,21 @@ public function addFieldToFilter($field, $condition = null)
265265

266266
$this->getSearchCriteriaBuilder();
267267
$this->getFilterBuilder();
268-
if (!is_array($condition) || !in_array(key($condition), ['from', 'to'])) {
269-
$this->filterBuilder->setField($field);
270-
$this->filterBuilder->setValue($condition);
271-
$this->searchCriteriaBuilder->addFilter($this->filterBuilder->create());
272-
} else {
268+
if (is_array($condition)
269+
&& in_array(key($condition), ['from', 'to'], true)
270+
) {
273271
if (!empty($condition['from'])) {
274-
$this->filterBuilder->setField("{$field}.from");
275-
$this->filterBuilder->setValue($condition['from']);
276-
$this->searchCriteriaBuilder->addFilter($this->filterBuilder->create());
272+
$this->addFieldToFilter("{$field}.from", $condition['from']);
277273
}
278274
if (!empty($condition['to'])) {
279-
$this->filterBuilder->setField("{$field}.to");
280-
$this->filterBuilder->setValue($condition['to']);
281-
$this->searchCriteriaBuilder->addFilter($this->filterBuilder->create());
275+
$this->addFieldToFilter("{$field}.to", $condition['to']);
282276
}
277+
} else {
278+
$this->filterBuilder->setField($field);
279+
$this->filterBuilder->setValue($condition);
280+
$this->searchCriteriaBuilder->addFilter($this->filterBuilder->create());
283281
}
282+
284283
return $this;
285284
}
286285

app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
3+
* Copyright © 2013-2018 Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
66

@@ -70,14 +70,18 @@ public function apply(
7070
[]
7171
);
7272
$isApplied = true;
73-
} elseif ('category_ids' === $field) {
73+
} elseif ('category_ids' === $field || $field === 'visibility') {
7474
$alias = $this->aliasResolver->getAlias($filter);
75-
$tableName = $this->resourceConnection->getTableName('catalog_category_product_index');
76-
$select->joinInner(
77-
[$alias => $tableName],
78-
'search_index.entity_id = category_ids_index.product_id',
79-
[]
80-
);
75+
if (!array_key_exists($alias, $select->getPart('from'))) {
76+
$tableName = $this->resourceConnection->getTableName(
77+
'catalog_category_product_index'
78+
);
79+
$select->joinInner(
80+
[$alias => $tableName],
81+
"search_index.entity_id = $alias.product_id",
82+
[]
83+
);
84+
}
8185
$isApplied = true;
8286
}
8387

app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
3+
* Copyright © 2013-2018 Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
66

@@ -63,7 +63,11 @@ public function aliasDataProvider()
6363
],
6464
'category_ids' => [
6565
'field' => 'category_ids',
66-
'alias' => 'category_ids_index',
66+
'alias' => 'category_products_index',
67+
],
68+
'visibility' => [
69+
'field' => 'visibility',
70+
'alias' => 'category_products_index',
6771
],
6872
];
6973
}

0 commit comments

Comments
 (0)