diff --git a/InventoryConfigurableProduct/Model/Inventory/ChangeParentProductStockStatus.php b/InventoryConfigurableProduct/Model/Inventory/ChangeParentProductStockStatus.php new file mode 100644 index 00000000000..88dc4a95e93 --- /dev/null +++ b/InventoryConfigurableProduct/Model/Inventory/ChangeParentProductStockStatus.php @@ -0,0 +1,152 @@ +request = $request; + $this->configurableType = $configurableType; + $this->stockRegistry = $stockRegistry; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->sourceItemRepository = $sourceItemRepository; + $this->stockItemRepository = $stockItemRepository; + $this->getSkusByProductIds = $getSkusByProductIds; + $this->collectionFactory = $collectionFactory; + } + + /** + * @inheritDoc + */ + public function execute($childproductId): void + { + $parentIds = $this->configurableType->getParentIdsByChild($childproductId); + foreach (array_unique($parentIds) as $productId) { + $this->processStockForParent((int)$childproductId, (int)$productId); + } + } + + /** + * Update stock status of configurable product based on children products stock status + * + * @param int $childproductId + * @param int $productId + * @return void + */ + private function processStockForParent(int $childproductId, int $productId): void + { + $childrenIsInStock = false; + + if ($sources = $this->request->getParam('sources')) { + if ($currentsourceItems = $sources['assigned_sources']) { + foreach ($currentsourceItems as $childItem) { + if ($childItem['status'] && $childItem['quantity'] > 0 && $childItem['source_status']) { + $childrenIsInStock = true; + break; + } + } + } + } + + if (!$childrenIsInStock) { + $sourceCodes = $this->collectionFactory->create() + ->addFieldToFilter(SourceInterface::ENABLED, 1) + ->addFieldToSelect('source_code') + ->getColumnValues('source_code'); + $childrenIds = $this->configurableType->getChildrenIds($productId); + $childrenSkus = $this->getSkusByProductIds->execute($childrenIds[0]); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(SourceItemInterface::SOURCE_CODE, $sourceCodes, 'in') + ->addFilter(SourceItemInterface::SKU, $childrenSkus, 'in') + ->addFilter(SourceItemInterface::SKU, $childrenSkus[$childproductId], 'neq') + ->addFilter(SourceItemInterface::STATUS, 1) + ->create(); + + $sourceItems = $this->sourceItemRepository->getList($searchCriteria)->getItems(); + foreach ($sourceItems as $childItem) { + if ($childItem->getStatus()) { + $childrenIsInStock = true; + break; + } + } + } + + $parentStockItem = $this->stockRegistry->getStockItem($productId); + $parentStockItem->setIsInStock($childrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } +} diff --git a/InventoryConfigurableProduct/Plugin/CatalogInventory/UpdateLegacyStockStatusForConfigurableProduct.php b/InventoryConfigurableProduct/Plugin/CatalogInventory/UpdateLegacyStockStatusForConfigurableProduct.php index b3d688f2766..9d0f520149f 100644 --- a/InventoryConfigurableProduct/Plugin/CatalogInventory/UpdateLegacyStockStatusForConfigurableProduct.php +++ b/InventoryConfigurableProduct/Plugin/CatalogInventory/UpdateLegacyStockStatusForConfigurableProduct.php @@ -14,6 +14,7 @@ use Magento\InventoryCatalogApi\Model\GetSkusByProductIdsInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\CatalogInventory\Model\Stock; +use Magento\InventoryConfigurableProduct\Model\Inventory\ChangeParentProductStockStatus; use Magento\InventoryConfigurableProduct\Model\IsProductSalableCondition\IsConfigurableProductChildrenSalable; use Magento\InventoryConfiguration\Model\GetLegacyStockItem; @@ -48,20 +49,28 @@ class UpdateLegacyStockStatusForConfigurableProduct */ private $isConfigurableProductChildrenSalable; + /** + * @var ChangeParentProductStockStatus + */ + private $changeParentProductStockStatus; + /** * @param GetProductTypeById $getProductTypeById * @param SetDataToLegacyStockStatus $setDataToLegacyStockStatus * @param GetSkusByProductIdsInterface $getSkusByProductIds * @param GetLegacyStockItem $getLegacyStockItem * @param IsConfigurableProductChildrenSalable $isConfigurableProductChildrenSalable + * @param ChangeParentProductStockStatus $changeParentProductStockStatus */ public function __construct( + ChangeParentProductStockStatus $changeParentProductStockStatus, GetProductTypeById $getProductTypeById, SetDataToLegacyStockStatus $setDataToLegacyStockStatus, GetSkusByProductIdsInterface $getSkusByProductIds, GetLegacyStockItem $getLegacyStockItem, IsConfigurableProductChildrenSalable $isConfigurableProductChildrenSalable ) { + $this->changeParentProductStockStatus = $changeParentProductStockStatus; $this->getProductTypeById = $getProductTypeById; $this->setDataToLegacyStockStatus = $setDataToLegacyStockStatus; $this->getSkusByProductIds = $getSkusByProductIds; @@ -82,11 +91,12 @@ public function __construct( */ public function afterSave(ItemResourceModel $subject, ItemResourceModel $result, StockItem $stockItem) { + $stockProductId = $stockItem->getProductId(); if ($stockItem->getIsInStock() && - $this->getProductTypeById->execute($stockItem->getProductId()) === Configurable::TYPE_CODE + $this->getProductTypeById->execute($stockProductId) === Configurable::TYPE_CODE ) { $productSku = $this->getSkusByProductIds - ->execute([$stockItem->getProductId()])[$stockItem->getProductId()]; + ->execute([$stockProductId])[$stockItem->getProductId()]; if ($stockItem->getStockStatusChangedAuto() || ($this->stockStatusChange($productSku) @@ -100,6 +110,7 @@ public function afterSave(ItemResourceModel $subject, ItemResourceModel $result, ); } } + $this->changeParentProductStockStatus->execute($stockItem->getProductId()); return $result; } diff --git a/InventoryConfigurableProduct/Test/Unit/Model/Inventory/ChangeParentProductStockStatusTest.php b/InventoryConfigurableProduct/Test/Unit/Model/Inventory/ChangeParentProductStockStatusTest.php new file mode 100644 index 00000000000..59e857beff9 --- /dev/null +++ b/InventoryConfigurableProduct/Test/Unit/Model/Inventory/ChangeParentProductStockStatusTest.php @@ -0,0 +1,206 @@ +requestMock = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configurableTypeMock = $this->getMockBuilder(Configurable::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockRegistryMock = $this->getMockBuilder(StockRegistryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + $this->sourceItemRepositoryMock = $this->getMockBuilder(SourceItemRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockItemRepositoryMock = $this->getMockBuilder(StockItemRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->getSkusByProductIdsMock = $this->getMockBuilder(GetSkusByProductIdsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) + ->setMethods(['addFieldToFilter','create','addFieldToSelect','getColumnValues']) + ->disableOriginalConstructor() + ->getMock(); + $this->changeParentProductStockStatus = $this->getMockBuilder(ChangeParentProductStockStatus::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->changeParentProductStockStatus = new ChangeParentProductStockStatus( + $this->requestMock, + $this->configurableTypeMock, + $this->stockRegistryMock, + $this->searchCriteriaBuilderMock, + $this->sourceItemRepositoryMock, + $this->stockItemRepositoryMock, + $this->getSkusByProductIdsMock, + $this->collectionFactoryMock + ); + } + + public function testChangeParentProductStockStatus() + { + $parentIds = [14]; + $childproductId = 13; + $childrenIds = [[13 => 13]]; + $childrenSkus = [13 => 'config-parent-blue']; + $sourcecodes = [0 => 'default',1 =>'north']; + $childrenIsInStock = false; + $sources = [ + 'assigned_sources' =>[ + [ + 'source_code' => 'north', + 'quantity' =>1 + ] + ] + ]; + $this->configurableTypeMock->expects($this->once()) + ->method('getParentIdsByChild') + ->with($childproductId) + ->willReturn($parentIds); + + $this->requestMock + ->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['sources', [], $sources] + ] + ); + + $this->collectionFactoryMock->expects($this->any())->method('create')->willReturn($this->collectionFactoryMock); + $this->collectionFactoryMock->expects($this->any())->method('addFieldToFilter') + ->with('enabled', 1) + ->willReturnSelf(); + $this->collectionFactoryMock->expects($this->any())->method('addFieldToSelect') + ->with('source_code') + ->willReturnSelf(); + $this->collectionFactoryMock->expects($this->any())->method('getColumnValues') + ->with('source_code') + ->willReturn($sourcecodes); +// + $this->configurableTypeMock->expects($this->any()) + ->method('getChildrenIds') + ->with($parentIds[0]) + ->willReturn($childrenIds); + $this->getSkusByProductIdsMock->expects($this->once()) + ->method('execute') + ->with($childrenIds[0]) + ->willReturn($childrenSkus); + + + $searchCriteriaMock = $this->createMock(SearchCriteria::class); + + $this->searchCriteriaBuilderMock->expects($this->any())->method('addFilter') + ->willReturnSelf(); + $this->searchCriteriaBuilderMock->expects($this->any())->method('create')->willReturn($searchCriteriaMock); + $searchResultsMock = $this->getMockForAbstractClass(SourceItemSearchResultsInterface::class); + $this->sourceItemRepositoryMock->expects($this->any()) + ->method('getList') + ->with($searchCriteriaMock) + ->willReturn($searchResultsMock); + + $sourceItems = $this->getMockForAbstractClass(SourceItemInterface::class); + $searchResultsMock->expects($this->once()) + ->method('getItems') + ->willReturn($sourceItems); + + $stockItemMock = $this->getMockBuilder(StockItemInterface::class) + ->setMethods(['setIsInStock', 'setStockStatusChangedAuto']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->stockRegistryMock->expects($this->any()) + ->method('getStockItem') + ->with($parentIds[0]) + ->willReturn($stockItemMock); + $stockItemMock->expects($this->any())->method('setIsInStock')->with($childrenIsInStock)->willReturnSelf(); + $stockItemMock->expects($this->any())->method('setStockStatusChangedAuto')->with(1)->willReturnSelf(); + $this->stockItemRepositoryMock->expects($this->any()) + ->method('save') + ->with($stockItemMock); + $this->changeParentProductStockStatus->execute($childproductId); + } +} diff --git a/InventoryConfigurableProduct/Test/Unit/Plugin/CatalogInventory/UpdateLegacyStockStatusForConfigurableProductTest.php b/InventoryConfigurableProduct/Test/Unit/Plugin/CatalogInventory/UpdateLegacyStockStatusForConfigurableProductTest.php index 6d6a5ae5316..45cbb289f2a 100644 --- a/InventoryConfigurableProduct/Test/Unit/Plugin/CatalogInventory/UpdateLegacyStockStatusForConfigurableProductTest.php +++ b/InventoryConfigurableProduct/Test/Unit/Plugin/CatalogInventory/UpdateLegacyStockStatusForConfigurableProductTest.php @@ -8,6 +8,7 @@ namespace Magento\InventoryConfigurableProduct\Test\Unit\Plugin\CatalogInventory; use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\InventoryConfigurableProduct\Model\Inventory\ChangeParentProductStockStatus; use Magento\InventoryConfigurableProduct\Model\IsProductSalableCondition\IsConfigurableProductChildrenSalable; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -60,11 +61,19 @@ class UpdateLegacyStockStatusForConfigurableProductTest extends TestCase */ private $plugin; + /** + * @var ChangeParentProductStockStatus + */ + private $changeParentProductStockStatus; + /** * @inheritdoc */ protected function setUp(): void { + $this->changeParentProductStockStatus = $this->getMockBuilder(ChangeParentProductStockStatus::class) + ->disableOriginalConstructor() + ->getMock(); $this->getProductTypeByIdMock = $this->getMockBuilder(GetProductTypeById::class) ->disableOriginalConstructor() ->getMock(); @@ -77,6 +86,7 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); $this->plugin = new UpdateLegacyStockStatusForConfigurableProduct( + $this->changeParentProductStockStatus, $this->getProductTypeByIdMock, $this->setDataToLegacyStockStatusMock, $this->getSkusByProductIdsMock, @@ -137,6 +147,9 @@ public function testConfigurableStockAfterLegacySockItemSave() $this->setDataToLegacyStockStatusMock->expects($this->once()) ->method('execute') ->with($product['sku'], (float) $product['qty'], Stock::STOCK_IN_STOCK); + $this->changeParentProductStockStatus->expects($this->atLeastOnce()) + ->method('execute') + ->with($product['id']); $this->plugin->afterSave($itemResourceModelMock, $itemResourceModelMock, $stockItemMock); } @@ -179,6 +192,9 @@ public function testConfigurableStockAfterLegacySockItemSaveNegativeScenario() ->method('execute') ->willReturn([$product['id'] => $product['sku']]); $stockItemMock->expects($this->never())->method('getQty'); + $this->changeParentProductStockStatus->expects($this->atLeastOnce()) + ->method('execute') + ->with($product['id']); $this->plugin->afterSave($itemResourceModelMock, $itemResourceModelMock, $stockItemMock); } }