diff --git a/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php
new file mode 100644
index 0000000000000..8fa90cf6531c4
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php
@@ -0,0 +1,77 @@
+criteriaInterfaceFactory = $criteriaInterfaceFactory;
+ $this->stockItemRepository = $stockItemRepository;
+ $this->stockConfiguration = $stockConfiguration;
+ }
+
+ /**
+ * Add stock items to products in collection.
+ *
+ * @param Observer $observer
+ * @return void
+ */
+ public function execute(Observer $observer)
+ {
+ /** @var Collection $productCollection */
+ $productCollection = $observer->getData('collection');
+ $productIds = array_keys($productCollection->getItems());
+ $criteria = $this->criteriaInterfaceFactory->create();
+ $criteria->setProductsFilter($productIds);
+ $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId());
+ $stockItemCollection = $this->stockItemRepository->getList($criteria);
+ foreach ($stockItemCollection->getItems() as $item) {
+ /** @var Product $product */
+ $product = $productCollection->getItemById($item->getProductId());
+ $productExtension = $product->getExtensionAttributes();
+ $productExtension->setStockItem($item);
+ $product->setExtensionAttributes($productExtension);
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php
new file mode 100644
index 0000000000000..8de05bd014039
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php
@@ -0,0 +1,165 @@
+criteriaInterfaceFactoryMock = $this->getMockBuilder(StockItemCriteriaInterfaceFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->stockItemRepositoryMock = $this->getMockBuilder(StockItemRepositoryInterface::class)
+ ->setMethods(['getList'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class)
+ ->setMethods(['getDefaultScopeId'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->subject = $objectManager->getObject(
+ AddStockItemsObserver::class,
+ [
+ 'criteriaInterfaceFactory' => $this->criteriaInterfaceFactoryMock,
+ 'stockItemRepository' => $this->stockItemRepositoryMock,
+ 'stockConfiguration' => $this->stockConfigurationMock
+ ]
+ );
+ }
+
+ /**
+ * Test AddStockItemsObserver::execute() add stock item to product as extension attribute.
+ */
+ public function testExecute()
+ {
+ $productId = 1;
+ $defaultScopeId = 0;
+
+ $criteria = $this->getMockBuilder(StockItemCriteriaInterface::class)
+ ->setMethods(['setProductsFilter', 'setScopeFilter'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $criteria->expects(self::once())
+ ->method('setProductsFilter')
+ ->with(self::identicalTo([$productId]))
+ ->willReturn(true);
+ $criteria->expects(self::once())
+ ->method('setScopeFilter')
+ ->with(self::identicalTo($defaultScopeId))
+ ->willReturn(true);
+
+ $this->criteriaInterfaceFactoryMock->expects(self::once())
+ ->method('create')
+ ->willReturn($criteria);
+ $stockItemCollection = $this->getMockBuilder(StockItemCollectionInterface::class)
+ ->setMethods(['getItems'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $stockItem = $this->getMockBuilder(StockItemInterface::class)
+ ->setMethods(['getProductId'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $stockItem->expects(self::once())
+ ->method('getProductId')
+ ->willReturn($productId);
+
+ $stockItemCollection->expects(self::once())
+ ->method('getItems')
+ ->willReturn([$stockItem]);
+
+ $this->stockItemRepositoryMock->expects(self::once())
+ ->method('getList')
+ ->with(self::identicalTo($criteria))
+ ->willReturn($stockItemCollection);
+
+ $this->stockConfigurationMock->expects(self::once())
+ ->method('getDefaultScopeId')
+ ->willReturn($defaultScopeId);
+
+ $productExtension = $this->getMockBuilder(ProductExtensionInterface::class)
+ ->setMethods(['setStockItem'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $productExtension->expects(self::once())
+ ->method('setStockItem')
+ ->with(self::identicalTo($stockItem));
+
+ $product = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $product->expects(self::once())
+ ->method('getExtensionAttributes')
+ ->willReturn($productExtension);
+ $product->expects(self::once())
+ ->method('setExtensionAttributes')
+ ->with(self::identicalTo($productExtension))
+ ->willReturnSelf();
+
+ /** @var ProductCollection|\PHPUnit_Framework_MockObject_MockObject $productCollection */
+ $productCollection = $this->getMockBuilder(ProductCollection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $productCollection->expects(self::once())
+ ->method('getItems')
+ ->willReturn([$productId => $product]);
+ $productCollection->expects(self::once())
+ ->method('getItemById')
+ ->with(self::identicalTo($productId))
+ ->willReturn($product);
+
+ /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observer */
+ $observer = $this->getMockBuilder(Observer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $observer->expects(self::once())
+ ->method('getData')
+ ->with('collection')
+ ->willReturn($productCollection);
+
+ $this->subject->execute($observer);
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/etc/events.xml b/app/code/Magento/CatalogInventory/etc/events.xml
index 3b5f2483ec57e..0a9f3c2d40dca 100644
--- a/app/code/Magento/CatalogInventory/etc/events.xml
+++ b/app/code/Magento/CatalogInventory/etc/events.xml
@@ -42,4 +42,7 @@
+
+
+
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_decimal_qty.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_decimal_qty.php
new file mode 100644
index 0000000000000..37ce93cc9c420
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_decimal_qty.php
@@ -0,0 +1,192 @@
+reinitialize();
+
+/** @var \Magento\TestFramework\ObjectManager $objectManager */
+$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+
+/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */
+$categoryLinkManagement = $objectManager->get(\Magento\Catalog\Api\CategoryLinkManagementInterface::class);
+
+$tierPrices = [];
+/** @var \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory */
+$tierPriceFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class);
+/** @var $tpExtensionAttributes */
+$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class);
+
+$adminWebsite = $objectManager->get(\Magento\Store\Api\WebsiteRepositoryInterface::class)->get('admin');
+$tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create()
+ ->setWebsiteId($adminWebsite->getId());
+
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL,
+ 'qty' => 2,
+ 'value' => 8,
+ ],
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes1);
+
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL,
+ 'qty' => 5,
+ 'value' => 5,
+ ],
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes1);
+
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID,
+ 'qty' => 3,
+ 'value' => 5,
+ ],
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes1);
+
+$tierPriceExtensionAttributes2 = $tpExtensionAttributesFactory->create()
+ ->setWebsiteId($adminWebsite->getId())
+ ->setPercentageValue(50);
+
+$tierPrices[] = $tierPriceFactory->create(
+ [
+ 'data' => [
+ 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID,
+ 'qty' => 10,
+ ],
+ ]
+)->setExtensionAttributes($tierPriceExtensionAttributes2);
+
+/** @var $product \Magento\Catalog\Model\Product */
+$product = $objectManager->create(\Magento\Catalog\Model\Product::class);
+$product->isObjectNew(true);
+$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
+ ->setId(1)
+ ->setAttributeSetId(4)
+ ->setWebsiteIds([1])
+ ->setName('Simple Product')
+ ->setSku('simple_with_decimal_qty')
+ ->setPrice(10)
+ ->setWeight(1)
+ ->setShortDescription("Short description")
+ ->setTaxClassId(0)
+ ->setTierPrices($tierPrices)
+ ->setDescription('Description with html tag')
+ ->setMetaTitle('meta title')
+ ->setMetaKeyword('meta keyword')
+ ->setMetaDescription('meta description')
+ ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
+ ->setStockData(
+ [
+ 'use_config_manage_stock' => 1,
+ 'qty' => 100,
+ 'is_qty_decimal' => 1,
+ 'is_in_stock' => 1,
+ ]
+ )->setCanSaveCustomOptions(true)
+ ->setHasOptions(true);
+
+$oldOptions = [
+ [
+ 'previous_group' => 'text',
+ 'title' => 'Test Field',
+ 'type' => 'field',
+ 'is_require' => 1,
+ 'sort_order' => 0,
+ 'price' => 1,
+ 'price_type' => 'fixed',
+ 'sku' => '1-text',
+ 'max_characters' => 100,
+ ],
+ [
+ 'previous_group' => 'date',
+ 'title' => 'Test Date and Time',
+ 'type' => 'date_time',
+ 'is_require' => 1,
+ 'sort_order' => 0,
+ 'price' => 2,
+ 'price_type' => 'fixed',
+ 'sku' => '2-date',
+ ],
+ [
+ 'previous_group' => 'select',
+ 'title' => 'Test Select',
+ 'type' => 'drop_down',
+ 'is_require' => 1,
+ 'sort_order' => 0,
+ 'values' => [
+ [
+ 'option_type_id' => null,
+ 'title' => 'Option 1',
+ 'price' => 3,
+ 'price_type' => 'fixed',
+ 'sku' => '3-1-select',
+ ],
+ [
+ 'option_type_id' => null,
+ 'title' => 'Option 2',
+ 'price' => 3,
+ 'price_type' => 'fixed',
+ 'sku' => '3-2-select',
+ ],
+ ],
+ ],
+ [
+ 'previous_group' => 'select',
+ 'title' => 'Test Radio',
+ 'type' => 'radio',
+ 'is_require' => 1,
+ 'sort_order' => 0,
+ 'values' => [
+ [
+ 'option_type_id' => null,
+ 'title' => 'Option 1',
+ 'price' => 3,
+ 'price_type' => 'fixed',
+ 'sku' => '4-1-radio',
+ ],
+ [
+ 'option_type_id' => null,
+ 'title' => 'Option 2',
+ 'price' => 3,
+ 'price_type' => 'fixed',
+ 'sku' => '4-2-radio',
+ ],
+ ],
+ ],
+];
+
+$options = [];
+
+/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */
+$customOptionFactory = $objectManager->create(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class);
+
+foreach ($oldOptions as $option) {
+ /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option */
+ $option = $customOptionFactory->create(['data' => $option]);
+ $option->setProductSku($product->getSku());
+
+ $options[] = $option;
+}
+
+$product->setOptions($options);
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */
+$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+$productRepository->save($product);
+
+$categoryLinkManagement->assignProductToCategories(
+ $product->getSku(),
+ [2]
+);
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_decimal_qty_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_decimal_qty_rollback.php
new file mode 100644
index 0000000000000..55bd53a2d5794
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_decimal_qty_rollback.php
@@ -0,0 +1,26 @@
+getInstance()->reinitialize();
+
+/** @var \Magento\Framework\Registry $registry */
+$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+try {
+ $product = $productRepository->get('simple_with_decimal_qty', false, null, true);
+ $productRepository->delete($product);
+} catch (NoSuchEntityException $e) {
+}
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/AddStockItemsObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/AddStockItemsObserverTest.php
new file mode 100644
index 0000000000000..71af5d102a8d7
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/AddStockItemsObserverTest.php
@@ -0,0 +1,41 @@
+create(Quote::class);
+ $quote->load('test01', 'reserved_order_id');
+ /** @var CollectionFactory $collectionFactory */
+ $collectionFactory = Bootstrap::getObjectManager()->create(CollectionFactory::class);
+ /** @var Collection $collection */
+ $collection = $collectionFactory->create();
+ $collection->setQuote($quote);
+ /** @var Quote\Item $quoteItem */
+ foreach ($collection->getItems() as $quoteItem) {
+ self::assertNotEmpty($quoteItem->getProduct()->getExtensionAttributes()->getStockItem());
+ self::assertInstanceOf(
+ StockItemInterface::class,
+ $quoteItem->getProduct()->getExtensionAttributes()->getStockItem()
+ );
+ }
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php
index a4dac0f285f58..28b2575643b05 100644
--- a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php
+++ b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php
@@ -390,7 +390,7 @@ public function testCreateOrderNewCustomerDifferentAddresses()
}
/**
- * @magentoDataFixture Magento/Catalog/_files/product_simple.php
+ * @magentoDataFixture Magento/Catalog/_files/product_simple_with_decimal_qty.php
* @magentoDbIsolation enabled
* @magentoAppIsolation enabled
*/
@@ -421,6 +421,10 @@ public function testCreateOrderNewCustomer()
$paymentMethod
);
$order = $this->_model->createOrder();
+ //Check, order considering decimal qty in product.
+ foreach ($order->getItems() as $orderItem) {
+ self::assertTrue($orderItem->getIsQtyDecimal());
+ }
$this->_verifyCreatedOrder($order, $shippingMethod);
}