diff --git a/src/Oro/Bundle/ProductBundle/ImportExport/Processor/ProductImportProcessor.php b/src/Oro/Bundle/ProductBundle/ImportExport/Processor/ProductImportProcessor.php new file mode 100644 index 00000000000..a6f71ee66de --- /dev/null +++ b/src/Oro/Bundle/ProductBundle/ImportExport/Processor/ProductImportProcessor.php @@ -0,0 +1,19 @@ +strategy instanceof ClosableInterface) { + $this->strategy->close(); + } + } +} diff --git a/src/Oro/Bundle/ProductBundle/ImportExport/Strategy/ProductStrategy.php b/src/Oro/Bundle/ProductBundle/ImportExport/Strategy/ProductStrategy.php index 60c5937f4d2..7a1ec155b03 100644 --- a/src/Oro/Bundle/ProductBundle/ImportExport/Strategy/ProductStrategy.php +++ b/src/Oro/Bundle/ProductBundle/ImportExport/Strategy/ProductStrategy.php @@ -5,6 +5,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Util\ClassUtils; +use Oro\Bundle\BatchBundle\Item\Support\ClosableInterface; use Oro\Bundle\LocaleBundle\ImportExport\Strategy\LocalizedFallbackValueAwareStrategy; use Oro\Bundle\OrganizationBundle\Entity\BusinessUnit; use Oro\Bundle\ProductBundle\Entity\Product; @@ -12,7 +13,7 @@ use Oro\Bundle\SecurityBundle\SecurityFacade; use Oro\Bundle\UserBundle\Entity\User; -class ProductStrategy extends LocalizedFallbackValueAwareStrategy +class ProductStrategy extends LocalizedFallbackValueAwareStrategy implements ClosableInterface { /** * @var SecurityFacade @@ -34,6 +35,19 @@ class ProductStrategy extends LocalizedFallbackValueAwareStrategy */ protected $productClass; + /** + * @var array|Product[] + */ + protected $processedProducts = []; + + /** + * {@inheritdoc} + */ + public function close() + { + $this->processedProducts = []; + } + /** * @param SecurityFacade $securityFacade */ @@ -89,7 +103,11 @@ protected function afterProcessEntity($entity) $event = new ProductStrategyEvent($entity, $this->context->getValue('itemData')); $this->eventDispatcher->dispatch(ProductStrategyEvent::PROCESS_AFTER, $event); - return parent::afterProcessEntity($entity); + /** @var Product $entity */ + $entity = parent::afterProcessEntity($entity); + $this->processedProducts[$entity->getSku()] = $entity; + + return $entity; } /** @@ -215,6 +233,25 @@ protected function updateRelations($entity, array $itemData = null) } } + /** + * {@inheritdoc} + */ + protected function processEntity( + $entity, + $isFullData = false, + $isPersistNew = false, + $itemData = null, + array $searchContext = [], + $entityIsRelation = false + ) { + if ($entity instanceof Product && array_key_exists($entity->getSku(), $this->processedProducts)) { + return $this->processedProducts[$entity->getSku()]; + } + + return parent::processEntity($entity, $isFullData, $isPersistNew, $itemData, $searchContext, $entityIsRelation); + } + + /** * Get additional search parameter name to find only related entities * diff --git a/src/Oro/Bundle/ProductBundle/Resources/config/importexport.yml b/src/Oro/Bundle/ProductBundle/Resources/config/importexport.yml index bd86d07dc13..0ce87f182f3 100644 --- a/src/Oro/Bundle/ProductBundle/Resources/config/importexport.yml +++ b/src/Oro/Bundle/ProductBundle/Resources/config/importexport.yml @@ -43,6 +43,7 @@ services: oro_product.importexport.processor.import.product: public: false parent: oro_importexport.processor.import_abstract + class: Oro\Bundle\ProductBundle\ImportExport\Processor\ProductImportProcessor calls: - [setDataConverter, ['@oro_product.importexport.data_converter.product']] - [setStrategy, ['@oro_product.importexport.strategy.product']] diff --git a/src/Oro/Bundle/ProductBundle/Tests/Functional/ImportExport/Strategy/ProductStrategyTest.php b/src/Oro/Bundle/ProductBundle/Tests/Functional/ImportExport/Strategy/ProductStrategyTest.php new file mode 100644 index 00000000000..fce8bd52919 --- /dev/null +++ b/src/Oro/Bundle/ProductBundle/Tests/Functional/ImportExport/Strategy/ProductStrategyTest.php @@ -0,0 +1,142 @@ +initClient(); + $this->loadFixtures([LoadProductData::class]); + $container = $this->getContainer(); + + $container->get('oro_importexport.field.database_helper')->onClear(); + $this->strategy = new ProductStrategy( + $container->get('event_dispatcher'), + $container->get('oro_importexport.strategy.import.helper'), + $container->get('oro_entity.helper.field_helper'), + $container->get('oro_importexport.field.database_helper'), + $container->get('oro_entity.entity_class_name_provider'), + $container->get('translator'), + $container->get('oro_importexport.strategy.new_entities_helper'), + $container->get('oro_entity.doctrine_helper') + ); + $this->strategy->setEntityName(Product::class); + $this->strategy->setVariantLinkClass(ProductVariantLink::class); + $this->strategy->setLocalizedFallbackValueClass(LocalizedFallbackValue::class); + $this->strategy->setSecurityFacade($container->get('oro_security.security_facade')); + } + + /** + * Impossible to import product when it's variant is in the same batch. + * Caused because variant is not in DB it could not be loaded by SKU and variant link contains invalid relation. + * + * @link https://magecore.atlassian.net/browse/BB-7908 + */ + public function testProcessWithVariantLinks() + { + $context = new Context([]); + $context->setValue('itemData', []); + $this->strategy->setImportExportContext($context); + + $inventoryStatusClassName = ExtendHelper::buildEnumValueClassName('prod_inventory_status'); + /** @var AbstractEnumValue $inventoryStatus */ + $inventoryStatus = $this->getContainer() + ->get('doctrine') + ->getRepository($inventoryStatusClassName) + ->find('in_stock'); + + /** @var ProductUnit $unit */ + $unit = $this->getReference(LoadProductUnits::BOX); + /** @var AttributeFamily $attributeFamily */ + $attributeFamily = $this->getEntity(AttributeFamily::class, ['code' => 'default_family']); + $newProductSku = 'PR-V1'; + + // Prepare new product that is imported in same batch and will be used later as variant link + $newProduct = $this->createProduct($newProductSku, $attributeFamily, $unit, $inventoryStatus); + /** @var Product $processedNewProduct */ + $processedNewProduct = $this->strategy->process($newProduct); + $this->assertEquals([], $context->getErrors()); + $this->assertInstanceOf(Product::class, $processedNewProduct); + $this->assertSame($newProductSku, $processedNewProduct->getSku()); + + // Get existing product that should be found by SKU as variant link relation + /** @var Product $existingProduct */ + $existingProduct = $this->getReference(LoadProductData::PRODUCT_1); + + $linkToNewProduct = new ProductVariantLink(); + $linkToNewProduct->setProduct((new Product())->setSku($newProductSku)); + $linkToExistingProduct = new ProductVariantLink(); + $linkToExistingProduct->setProduct((new Product())->setSku($existingProduct->getSku())); + + // Add prepared variant links to newly imported product + $productWithVariants = $this->createProduct('PR-VV', $attributeFamily, $unit, $inventoryStatus); + $productWithVariants->addVariantLink($linkToNewProduct); + $productWithVariants->addVariantLink($linkToExistingProduct); + + // Check that all variant links present and were attached correctly + /** @var Product $processedProductWithVariants */ + $processedProductWithVariants = $this->strategy->process($productWithVariants); + $this->assertEquals([], $context->getErrors()); + $this->assertInstanceOf(Product::class, $processedProductWithVariants); + $this->assertSame($productWithVariants->getSku(), $processedProductWithVariants->getSku()); + $this->assertCount(2, $processedProductWithVariants->getVariantLinks()); + $usedVariantLinksProductSkus = array_map( + function (ProductVariantLink $variantLink) { + return $variantLink->getProduct()->getSku(); + }, + $processedProductWithVariants->getVariantLinks()->toArray() + ); + $this->assertContains($newProductSku, $usedVariantLinksProductSkus); + $this->assertContains($existingProduct->getSku(), $usedVariantLinksProductSkus); + } + + /** + * @param string $sku + * @param AttributeFamily $attributeFamily + * @param ProductUnit $unit + * @param AbstractEnumValue $inventoryStatus + * @return Product + */ + protected function createProduct( + $sku, + AttributeFamily $attributeFamily, + ProductUnit $unit, + AbstractEnumValue $inventoryStatus + ) { + $newProduct = new Product(); + $newProduct->setSku($sku); + $newProduct->setAttributeFamily($attributeFamily); + $newProduct->setInventoryStatus($inventoryStatus); + $newProduct->setPrimaryUnitPrecision( + (new ProductUnitPrecision())->setUnit($unit) + ); + + return $newProduct; + } +} diff --git a/src/Oro/Bundle/ProductBundle/Tests/Unit/ImportExport/Processor/ProductImportProcessorTest.php b/src/Oro/Bundle/ProductBundle/Tests/Unit/ImportExport/Processor/ProductImportProcessorTest.php new file mode 100644 index 00000000000..dad1d02d828 --- /dev/null +++ b/src/Oro/Bundle/ProductBundle/Tests/Unit/ImportExport/Processor/ProductImportProcessorTest.php @@ -0,0 +1,42 @@ +processor = new ProductImportProcessor(); + } + + public function testCloseWithClosableStrategy() + { + /** @var ProductStrategy|\PHPUnit_Framework_MockObject_MockObject $strategy */ + $strategy = $this->getMockBuilder(ProductStrategy::class) + ->disableOriginalConstructor() + ->getMock(); + $strategy->expects($this->once()) + ->method('close'); + $this->processor->setStrategy($strategy); + $this->processor->close(); + } + + public function testCloseWithNonClosableStrategy() + { + /** @var StrategyInterface|\PHPUnit_Framework_MockObject_MockObject $strategy */ + $strategy = $this->createMock(StrategyInterface::class); + $strategy->expects($this->never()) + ->method($this->anything()); + $this->processor->setStrategy($strategy); + $this->processor->close(); + } +}