From d5c9eef075cd5f4d9781ec7f3151a14e087d061d Mon Sep 17 00:00:00 2001 From: Magento EngCom Team Date: Tue, 23 Jan 2018 11:04:19 -0600 Subject: [PATCH] :arrow_double_up: Forwardport of magento/magento2#11997 to 2.3-develop branch Applied pull request patch https://github.com/magento/magento2/pull/11997.patch (created by @nmalevanec) based on commit(s): 1. 0db2b08cb125046077b0abf1039718a3f6805cf9 Fixed GitHub Issues in 2.3-develop branch: - magento/magento2#11941: Invoice for products that use qty decimal rounds down to whole number (reported by @ralbin) --- .../Observer/AddStockItemsObserver.php | 77 +++++++ .../Observer/AddStockItemsObserverTest.php | 165 +++++++++++++++ .../Magento/CatalogInventory/etc/events.xml | 3 + .../product_simple_with_decimal_qty.php | 192 ++++++++++++++++++ ...oduct_simple_with_decimal_qty_rollback.php | 26 +++ .../Observer/AddStockItemsObserverTest.php | 41 ++++ .../Sales/Model/AdminOrder/CreateTest.php | 6 +- 7 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php create mode 100644 app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_decimal_qty.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_decimal_qty_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/AddStockItemsObserverTest.php 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 a2c33e3f7db9f..3197501e9b70b 100644 --- a/app/code/Magento/CatalogInventory/etc/events.xml +++ b/app/code/Magento/CatalogInventory/etc/events.xml @@ -39,4 +39,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 946d15b6a81d7..ee7ddc1ba1aba 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php @@ -394,7 +394,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 */ @@ -425,6 +425,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); }