diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 62ad3ab4..f3c65f04 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -2,7 +2,7 @@ name: PHP Composer on: push: - branches: [ master 4.2 4.1 3.1 1.6 ] + branches: [ master 5.0 4.2 4.1 3.1 1.6 ] pull_request: jobs: @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Install dependencies - run: php -r "copy('https://cs.symfony.com/download/php-cs-fixer-v3.phar', 'php-cs-fixer.phar');" + - name: Install dependencies + run: php -r "copy('https://cs.symfony.com/download/php-cs-fixer-v3.phar', 'php-cs-fixer.phar');" - - name: Run php-cs-fixer - run: php php-cs-fixer.phar fix --dry-run --config=.php_cs.php --cache-file=.php_cs.cache --verbose --show-progress=dots --diff --allow-risky=yes + - name: Run php-cs-fixer + run: php php-cs-fixer.phar fix --dry-run --config=.php_cs.php --cache-file=.php_cs.cache --verbose --show-progress=dots --diff --allow-risky=yes diff --git a/Command/CleanupCommand.php b/Command/CleanupCommand.php index aed4ced7..071951a0 100644 --- a/Command/CleanupCommand.php +++ b/Command/CleanupCommand.php @@ -17,7 +17,7 @@ class CleanupCommand extends Command implements CronCommandInterface { /** @var string */ - protected static $defaultName = 'oro:akeneo:cleanup'; + protected static $defaultName = 'oro:cron:akeneo:cleanup'; /** @var DoctrineHelper */ private $doctrineHelper; @@ -46,6 +46,7 @@ public function configure() <<<'HELP' The %command.name% command clears fields changes for complete job records from oro_integration_fields_changes table. + php %command.full_name% HELP ); diff --git a/Controller/ValidateConnectionController.php b/Controller/ValidateConnectionController.php index 7cb5c322..ab931f96 100644 --- a/Controller/ValidateConnectionController.php +++ b/Controller/ValidateConnectionController.php @@ -113,7 +113,7 @@ public function validateConnectionAction(Request $request, Channel $channel = nu 'akeneoLocales' => $akeneoLocales, 'success' => $success, 'message' => $message, - 'currencyList' => $this->currencyProvider->getCurrencies(), + 'currencyList' => $this->currencyProvider->getCurrencyList(), ] ); } diff --git a/Entity/AkeneoSettings.php b/Entity/AkeneoSettings.php index 63d1b586..3566fc23 100644 --- a/Entity/AkeneoSettings.php +++ b/Entity/AkeneoSettings.php @@ -89,6 +89,12 @@ class AkeneoSettings extends Transport * @ORM\Column(name="akeneo_product_filter", type="text", nullable=true) */ protected $productFilter; + /** + * @var string + * + * @ORM\Column(name="akeneo_conf_product_filter", type="text", nullable=true) + */ + protected $configurableProductFilter; /** * @var string * @@ -282,6 +288,26 @@ public function setProductFilter($productFilter) return $this; } + /** + * @return string + */ + public function getConfigurableProductFilter() + { + return $this->configurableProductFilter; + } + + /** + * @param string $configurableProductFilter + * + * @return self + */ + public function setConfigurableProductFilter($configurableProductFilter) + { + $this->configurableProductFilter = $configurableProductFilter; + + return $this; + } + /** * @return ParameterBag */ diff --git a/EntityConfig/ImportexportFieldConfiguration.php b/EntityConfig/ImportexportFieldConfiguration.php index 0632f382..1eb9ed2c 100644 --- a/EntityConfig/ImportexportFieldConfiguration.php +++ b/EntityConfig/ImportexportFieldConfiguration.php @@ -19,10 +19,10 @@ public function configure(NodeBuilder $nodeBuilder): void { $nodeBuilder ->scalarNode('source') - ->info('`string` source of field.') + ->info('`string` source of field.') ->end() ->scalarNode('source_name') - ->info('`string` source name of field.') + ->info('`string` source name of field.') ->end(); } } diff --git a/Form/Extension/ChannelTypeExtension.php b/Form/Extension/ChannelTypeExtension.php index 86ec2363..22912205 100644 --- a/Form/Extension/ChannelTypeExtension.php +++ b/Form/Extension/ChannelTypeExtension.php @@ -15,10 +15,12 @@ class ChannelTypeExtension extends AbstractTypeExtension * @var array */ protected $connectorsOrder = [ + 'brand', 'category', 'attribute', 'attribute_family', 'product', + 'configurable_product', ]; /** diff --git a/Form/Type/AkeneoSettingsType.php b/Form/Type/AkeneoSettingsType.php index 16870316..7152b732 100644 --- a/Form/Type/AkeneoSettingsType.php +++ b/Form/Type/AkeneoSettingsType.php @@ -261,6 +261,15 @@ public function buildForm(FormBuilderInterface $builder, array $options) ], ] ) + ->add( + 'configurableProductFilter', + TextareaType::class, + [ + 'required' => false, + 'label' => 'oro.akeneo.integration.settings.akeneo_configurable_product_filter.label', + 'constraints' => [new JsonConstraint()], + ] + ) ->add( 'priceList', PriceListSelectType::class, diff --git a/ImportExport/EventListener/OwnerStrategyEventListener.php b/ImportExport/EventListener/OwnerStrategyEventListener.php index 5c3dc74c..42719a35 100644 --- a/ImportExport/EventListener/OwnerStrategyEventListener.php +++ b/ImportExport/EventListener/OwnerStrategyEventListener.php @@ -37,7 +37,7 @@ public function onProcessBefore(StrategyEvent $event) protected function getChannel(ContextInterface $context) { - if (!$this->channel) { + if (!$this->channel && $context->getOption('channel')) { $this->channel = $this->doctrineHelper->getEntityReference( Channel::class, $context->getOption('channel') diff --git a/ImportExport/Processor/AsyncProcessor.php b/ImportExport/Processor/AsyncProcessor.php index 1c73739d..31fab7b7 100644 --- a/ImportExport/Processor/AsyncProcessor.php +++ b/ImportExport/Processor/AsyncProcessor.php @@ -6,51 +6,8 @@ class AsyncProcessor implements ProcessorInterface { - use CacheProviderAwareProcessor; - - /** @var array */ - private $variants = []; - public function process($item) { - $this->updateVariants($item); - return $item; } - - private function updateVariants(array &$item) - { - $sku = $item['sku']; - - if (!empty($item['family_variant'])) { - if (isset($item['parent'], $this->variants[$sku])) { - $parent = $item['parent']; - foreach (array_keys($this->variants[$sku]) as $sku) { - $this->variants[$parent][$sku] = ['parent' => $parent, 'variant' => $sku]; - } - } - - return; - } - - if (empty($item['parent'])) { - return; - } - - $parent = $item['parent']; - - $this->variants[$parent][$sku] = ['parent' => $parent, 'variant' => $sku]; - } - - public function initialize() - { - $this->variants = []; - $this->cacheProvider->delete('product_variants'); - } - - public function flush() - { - $this->cacheProvider->save('product_variants', $this->variants); - $this->variants = []; - } } diff --git a/ImportExport/Processor/BuildVariantCacheProcessor.php b/ImportExport/Processor/BuildVariantCacheProcessor.php new file mode 100644 index 00000000..b029144c --- /dev/null +++ b/ImportExport/Processor/BuildVariantCacheProcessor.php @@ -0,0 +1,56 @@ +updateVariants($item); + + return $item; + } + + private function updateVariants(array &$item) + { + $sku = $item['sku']; + + if (!empty($item['family_variant'])) { + if (isset($item['parent'], $this->variants[$sku])) { + $parent = $item['parent']; + foreach (array_keys($this->variants[$sku]) as $sku) { + $this->variants[$parent][$sku] = ['parent' => $parent, 'variant' => $sku]; + } + } + + return; + } + + if (empty($item['parent'])) { + return; + } + + $parent = $item['parent']; + + $this->variants[$parent][$sku] = ['parent' => $parent, 'variant' => $sku]; + } + + public function initialize() + { + $this->variants = []; + $this->cacheProvider->delete('product_variants'); + } + + public function flush() + { + $this->cacheProvider->save('product_variants', $this->variants); + $this->variants = []; + } +} diff --git a/ImportExport/Processor/ProductVariantProcessor.php b/ImportExport/Processor/ProductVariantProcessor.php index 5e564705..cce2968b 100644 --- a/ImportExport/Processor/ProductVariantProcessor.php +++ b/ImportExport/Processor/ProductVariantProcessor.php @@ -8,6 +8,7 @@ use Oro\Bundle\ImportExportBundle\Context\ContextRegistry; use Oro\Bundle\ImportExportBundle\Processor\ProcessorInterface; use Oro\Bundle\ImportExportBundle\Strategy\Import\ImportStrategyHelper; +use Oro\Bundle\IntegrationBundle\Entity\Channel; use Oro\Bundle\ProductBundle\Entity\Product; use Oro\Bundle\ProductBundle\Entity\ProductVariantLink; use Oro\Bundle\ProductBundle\Entity\Repository\ProductRepository; @@ -30,6 +31,9 @@ class ProductVariantProcessor implements ProcessorInterface, StepExecutionAwareI /** @var TranslatorInterface */ private $translator; + /** @var int */ + private $organizationId; + public function __construct( ManagerRegistry $registry, ImportStrategyHelper $strategyHelper, @@ -67,7 +71,7 @@ public function process($items) /** @var ProductRepository $productRepository */ $productRepository = $objectManager->getRepository(Product::class); - $parentProduct = $productRepository->findOneBySku($parentSku); + $parentProduct = $this->findProductBySku($productRepository,$parentSku); if (!$parentProduct instanceof Product) { $context->incrementErrorEntriesCount(); $errorMessages = [ @@ -110,7 +114,7 @@ function ($variantSku) { continue; } - $variantProduct->setStatus(Product::STATUS_ENABLED); +// $variantProduct->setStatus(Product::STATUS_ENABLED); unset($variantSkusUppercase[$variantProduct->getSkuUppercase()]); } @@ -139,7 +143,7 @@ function ($variantSku) { $variantProduct->addParentVariantLink($variantLink); $parentProduct->addVariantLink($variantLink); - $variantProduct->setStatus(Product::STATUS_ENABLED); +// $variantProduct->setStatus(Product::STATUS_ENABLED); $context->incrementAddCount(); @@ -153,7 +157,7 @@ function ($variantSku) { $objectManager->clear(); - $parentProduct = $productRepository->findOneBySku($parentSku); + $parentProduct = $this->findProductBySku($productRepository,$parentSku); if (!$parentProduct instanceof Product) { return null; } @@ -181,4 +185,35 @@ function ($variantSku) { return $parentProduct; } + + private function findProductBySku(ProductRepository $productRepository, $parentSku) + { + $organizationId = $this->getOrganizationId(); + + $qb = $productRepository->getBySkuQueryBuilder($parentSku); + $qb->andWhere($qb->expr()->eq('product.organization', ':organization')) + ->setParameter('organization', $organizationId); + + return $qb->getQuery()->getOneOrNullResult(); + } + + private function getOrganizationId(): ?int + { + if (!$this->organizationId) { + $channelId = $this->stepExecution->getJobExecution()->getExecutionContext()->get('channel'); + if (!$channelId) { + return null; + } + + /** @var Channel $channel */ + $channel = $this->registry->getRepository(Channel::class)->find($channelId); + if (!$channel) { + return null; + } + + $this->organizationId = $channel->getOrganization()->getId(); + } + + return $this->organizationId; + } } diff --git a/ImportExport/Reader/ProductVariantReader.php b/ImportExport/Reader/ProductVariantReader.php index b28d4379..78fc28a0 100644 --- a/ImportExport/Reader/ProductVariantReader.php +++ b/ImportExport/Reader/ProductVariantReader.php @@ -13,7 +13,8 @@ protected function initializeFromContext(ContextInterface $context) { parent::initializeFromContext($context); - $variants = $this->cacheProvider->fetch('akeneo')['variants'] ?? []; + $cache = $this->cacheProvider->fetch('product_variants') ; + $variants = $cache !== false ? $cache : []; $this->stepExecution->setReadCount(count($variants)); diff --git a/ImportExport/Writer/AsyncWriter.php b/ImportExport/Writer/AsyncWriter.php index bf88761a..5bb56f57 100644 --- a/ImportExport/Writer/AsyncWriter.php +++ b/ImportExport/Writer/AsyncWriter.php @@ -5,7 +5,7 @@ use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Types\Types; use Oro\Bundle\AkeneoBundle\Async\Topics; -use Oro\Bundle\AkeneoBundle\Tools\CacheProviderTrait; +use Oro\Bundle\AkeneoBundle\EventListener\AdditionalOptionalListenerManager; use Oro\Bundle\BatchBundle\Entity\StepExecution; use Oro\Bundle\BatchBundle\Item\ItemWriterInterface; use Oro\Bundle\BatchBundle\Item\Support\ClosableInterface; @@ -14,6 +14,7 @@ use Oro\Bundle\IntegrationBundle\Entity\FieldsChanges; use Oro\Bundle\MessageQueueBundle\Client\BufferedMessageProducer; use Oro\Bundle\MessageQueueBundle\Entity\Job; +use Oro\Bundle\PlatformBundle\Manager\OptionalListenerManager; use Oro\Component\MessageQueue\Client\Message; use Oro\Component\MessageQueue\Client\MessagePriority; use Oro\Component\MessageQueue\Client\MessageProducerInterface; @@ -23,10 +24,6 @@ class AsyncWriter implements ClosableInterface, StepExecutionAwareInterface { - use CacheProviderTrait; - - private const VARIANTS_BATCH_SIZE = 25; - /** @var MessageProducerInterface * */ private $messageProducer; @@ -39,17 +36,30 @@ class AsyncWriter implements /** @var DoctrineHelper */ private $doctrineHelper; + /** @var OptionalListenerManager */ + private $optionalListenerManager; + + /** @var AdditionalOptionalListenerManager */ + private $additionalOptionalListenerManager; + public function __construct( MessageProducerInterface $messageProducer, - DoctrineHelper $doctrineHelper + DoctrineHelper $doctrineHelper, + OptionalListenerManager $optionalListenerManager, + AdditionalOptionalListenerManager $additionalOptionalListenerManager ) { $this->messageProducer = $messageProducer; $this->doctrineHelper = $doctrineHelper; + $this->optionalListenerManager = $optionalListenerManager; + $this->additionalOptionalListenerManager = $additionalOptionalListenerManager; } public function initialize() { $this->size = 0; + + $this->additionalOptionalListenerManager->disableListeners(); + $this->optionalListenerManager->disableListeners($this->optionalListenerManager->getListeners()); } public function write(array $items) @@ -67,52 +77,30 @@ public function write(array $items) $this->stepExecution->setWriteCount($this->size); $jobId = $this->insertJob($jobName); - $this->createFieldsChanges($jobId, $items, 'items'); - $this->sendMessage($channelId, $jobId, true); - } - - public function flush() - { - $this->size = 0; - - $variants = $this->cacheProvider->fetch('product_variants') ?? []; - if (!$variants) { - return; - } - - $channelId = $this->stepExecution->getJobExecution()->getExecutionContext()->get('channel'); - - $chunks = array_chunk($variants, self::VARIANTS_BATCH_SIZE, true); - - foreach ($chunks as $key => $chunk) { - $jobName = sprintf( - 'oro_integration:sync_integration:%s:variants:%s-%s', - $channelId, - self::VARIANTS_BATCH_SIZE * $key + 1, - self::VARIANTS_BATCH_SIZE * $key + count($chunk) - ); - - $jobId = $this->insertJob($jobName); - $this->createFieldsChanges($jobId, $chunk, 'variants'); - $this->sendMessage($channelId, $jobId); + if ($jobId && $this->createFieldsChanges($jobId, $items, 'items')) { + $this->sendMessage($channelId, $jobId, true); } } - private function createFieldsChanges(int $jobId, array &$data, string $key): void + private function createFieldsChanges(int $jobId, array &$data, string $key): bool { $em = $this->doctrineHelper->getEntityManager(FieldsChanges::class); $fieldsChanges = $em ->getRepository(FieldsChanges::class) ->findOneBy(['entityId' => $jobId, 'entityClass' => Job::class]); - if (!$fieldsChanges) { - $fieldsChanges = new FieldsChanges([]); - $fieldsChanges->setEntityClass(Job::class); - $fieldsChanges->setEntityId($jobId); + if ($fieldsChanges) { + return false; } + + $fieldsChanges = new FieldsChanges([]); + $fieldsChanges->setEntityClass(Job::class); + $fieldsChanges->setEntityId($jobId); $fieldsChanges->setChangedFields([$key => $data]); $em->persist($fieldsChanges); $em->flush($fieldsChanges); $em->clear(FieldsChanges::class); + + return true; } private function sendMessage(int $channelId, int $jobId, bool $incrementedRead = false): void @@ -143,12 +131,15 @@ private function getRootJob(): ?int throw new \InvalidArgumentException('Root job id is empty'); } - return $rootJobId; + return (int)$rootJobId; } public function close() { $this->size = 0; + + $this->optionalListenerManager->enableListeners($this->optionalListenerManager->getListeners()); + $this->additionalOptionalListenerManager->enableListeners(); } public function setStepExecution(StepExecution $stepExecution) @@ -159,12 +150,34 @@ public function setStepExecution(StepExecution $stepExecution) private function insertJob(string $jobName): ?int { $em = $this->doctrineHelper->getEntityManager(Job::class); - $tableName = $em->getClassMetadata(Job::class)->getTableName(); $connection = $em->getConnection(); + $rootJobId = $this->getRootJob(); + + $hasRootJob = $connection + ->executeStatement( + 'SELECT 1 FROM oro_message_queue_job WHERE id = :id LIMIT 1;', + ['id' => $rootJobId], + ['id' => Types::INTEGER] + ); + + if (!$hasRootJob) { + throw new \InvalidArgumentException(sprintf('Root job "%d" missing', $rootJobId)); + } + + $childJob = $connection + ->executeStatement( + 'SELECT id FROM oro_message_queue_job WHERE root_job_id = :rootJob and name = :name LIMIT 1;', + ['rootJob' => $rootJobId, 'name' => $jobName], + ['rootJob' => Types::INTEGER, 'name' => Types::STRING] + ); + + if ($childJob) { + return $childJob; + } $qb = $connection->createQueryBuilder(); $qb - ->insert($tableName) + ->insert('oro_message_queue_job') ->values([ 'name' => ':name', 'status' => ':status', @@ -178,7 +191,7 @@ private function insertJob(string $jobName): ?int 'interrupted' => false, 'unique' => false, 'createdAt' => new \DateTime(), - 'rootJob' => $this->getRootJob(), + 'rootJob' => $rootJobId, ], [ 'name' => Types::STRING, 'status' => Types::STRING, diff --git a/ImportExport/Writer/AttributeWriter.php b/ImportExport/Writer/AttributeWriter.php index c78ca4f0..abc7a712 100644 --- a/ImportExport/Writer/AttributeWriter.php +++ b/ImportExport/Writer/AttributeWriter.php @@ -278,8 +278,8 @@ protected function setAttributeData(FieldConfigModel $fieldConfigModel) $attributeConfig->set('field_name', $fieldName); $attributeConfig->set('is_attribute', true); - $attributeConfig->set('is_global', false); - $attributeConfig->set('organization_id', $this->getOrganizationId()); + $attributeConfig->set('is_global', true); + $attributeConfig->remove('organization_id'); $this->configManager->persist($attributeConfig); parent::setAttributeData($fieldConfigModel); diff --git a/ImportExport/Writer/ConfigurableAsyncWriter.php b/ImportExport/Writer/ConfigurableAsyncWriter.php new file mode 100644 index 00000000..c1e55588 --- /dev/null +++ b/ImportExport/Writer/ConfigurableAsyncWriter.php @@ -0,0 +1,240 @@ +messageProducer = $messageProducer; + $this->doctrineHelper = $doctrineHelper; + $this->optionalListenerManager = $optionalListenerManager; + $this->additionalOptionalListenerManager = $additionalOptionalListenerManager; + } + + public function initialize() + { + $this->variants = []; + + $this->additionalOptionalListenerManager->disableListeners(); + $this->optionalListenerManager->disableListeners($this->optionalListenerManager->getListeners()); + } + + public function write(array $items) + { + foreach ($items as $item) { + $sku = $item['sku']; + + if (!empty($item['family_variant'])) { + if (isset($item['parent'], $this->variants[$sku])) { + $parent = $item['parent']; + foreach (array_keys($this->variants[$sku]) as $sku) { + $this->variants[$parent][$sku] = ['parent' => $parent, 'variant' => $sku]; + } + } + + return; + } + + if (empty($item['parent'])) { + return; + } + + $parent = $item['parent']; + + $this->variants[$parent][$sku] = ['parent' => $parent, 'variant' => $sku]; + } + } + + public function close() + { + $this->variants = []; + + $this->optionalListenerManager->enableListeners($this->optionalListenerManager->getListeners()); + $this->additionalOptionalListenerManager->enableListeners(); + } + + public function flush() + { + $channelId = $this->stepExecution->getJobExecution()->getExecutionContext()->get('channel'); + + $chunks = array_chunk($this->variants, self::VARIANTS_BATCH_SIZE, true); + + foreach ($chunks as $key => $chunk) { + $jobName = sprintf( + 'oro_integration:sync_integration:%s:variants:%s-%s', + $channelId, + self::VARIANTS_BATCH_SIZE * $key + 1, + self::VARIANTS_BATCH_SIZE * $key + count($chunk) + ); + + $jobId = $this->insertJob($jobName); + if ($jobId && $this->createFieldsChanges($jobId, $chunk, 'variants')) { + $this->sendMessage($channelId, $jobId); + } + } + } + + private function createFieldsChanges(int $jobId, array &$data, string $key): bool + { + $em = $this->doctrineHelper->getEntityManager(FieldsChanges::class); + $fieldsChanges = $em + ->getRepository(FieldsChanges::class) + ->findOneBy(['entityId' => $jobId, 'entityClass' => Job::class]); + if ($fieldsChanges) { + return false; + } + + $fieldsChanges = new FieldsChanges([]); + $fieldsChanges->setEntityClass(Job::class); + $fieldsChanges->setEntityId($jobId); + $fieldsChanges->setChangedFields([$key => $data]); + $em->persist($fieldsChanges); + $em->flush($fieldsChanges); + $em->clear(FieldsChanges::class); + + return true; + } + + private function sendMessage(int $channelId, int $jobId, bool $incrementedRead = false): void + { + $this->messageProducer->send( + Topics::IMPORT_PRODUCTS, + new Message( + [ + 'integrationId' => $channelId, + 'jobId' => $jobId, + 'connector' => 'configurable_product', + 'connector_parameters' => ['incremented_read' => $incrementedRead], + ], + MessagePriority::HIGH + ) + ); + + if ($this->messageProducer instanceof BufferedMessageProducer + && $this->messageProducer->isBufferingEnabled()) { + $this->messageProducer->flushBuffer(); + } + } + + private function getRootJob(): ?int + { + $rootJobId = $this->stepExecution->getJobExecution()->getExecutionContext()->get('rootJobId') ?? null; + if (!$rootJobId) { + throw new \InvalidArgumentException('Root job id is empty'); + } + + return (int)$rootJobId; + } + + public function setStepExecution(StepExecution $stepExecution) + { + $this->stepExecution = $stepExecution; + } + + private function insertJob(string $jobName): ?int + { + $em = $this->doctrineHelper->getEntityManager(Job::class); + $connection = $em->getConnection(); + $rootJobId = $this->getRootJob(); + + $hasRootJob = $connection + ->executeStatement( + 'SELECT 1 FROM oro_message_queue_job WHERE id = :id LIMIT 1;', + ['id' => $rootJobId], + ['id' => Types::INTEGER] + ); + + if (!$hasRootJob) { + throw new \InvalidArgumentException(sprintf('Root job "%d" missing', $rootJobId)); + } + + $childJob = $connection + ->executeStatement( + 'SELECT id FROM oro_message_queue_job WHERE root_job_id = :rootJob and name = :name LIMIT 1;', + ['rootJob' => $rootJobId, 'name' => $jobName], + ['rootJob' => Types::INTEGER, 'name' => Types::STRING] + ); + + if ($childJob) { + return $childJob; + } + + $qb = $connection->createQueryBuilder(); + $qb + ->insert('oro_message_queue_job') + ->values([ + 'name' => ':name', + 'status' => ':status', + 'interrupted' => ':interrupted', + 'created_at' => ':createdAt', + 'root_job_id' => ':rootJob', + ]) + ->setParameters([ + 'name' => $jobName, + 'status' => Job::STATUS_NEW, + 'interrupted' => false, + 'unique' => false, + 'createdAt' => new \DateTime(), + 'rootJob' => $rootJobId, + ], [ + 'name' => Types::STRING, + 'status' => Types::STRING, + 'interrupted' => Types::BOOLEAN, + 'unique' => Types::BOOLEAN, + 'createdAt' => Types::DATETIME_MUTABLE, + 'rootJob' => Types::INTEGER, + ]); + + if ($connection->getDatabasePlatform() instanceof MySqlPlatform) { + $qb->setValue('`unique`', ':unique'); + } else { + $qb->setValue('"unique"', ':unique'); + } + + $qb->execute(); + + return $connection->lastInsertId(); + } +} diff --git a/ImportExport/Writer/EmptyWriter.php b/ImportExport/Writer/EmptyWriter.php new file mode 100644 index 00000000..0d6ef822 --- /dev/null +++ b/ImportExport/Writer/EmptyWriter.php @@ -0,0 +1,15 @@ +initAttributesList(); $this->initMeasureFamilies(); - $searchFilters = $this->akeneoSearchBuilder->getFilters((new ParseUpdatedPlaceholder($this->transportEntity->getProductFilter(), $updatedAt))()); + $queryParams = [ + 'scope' => $this->transportEntity->getAkeneoActiveChannel(), + 'search' => $this->akeneoSearchBuilder->getFilters((new ParseUpdatedPlaceholder($this->transportEntity->getProductFilter(), $updatedAt))()), + ]; if ($this->transportEntity->getSyncProducts() === SyncProductsDataProvider::PUBLISHED) { return new ProductIterator( - $this->client->getPublishedProductApi()->all( - $pageSize, - ['search' => $searchFilters, 'scope' => $this->transportEntity->getAkeneoActiveChannel()] - ), + $this->client->getPublishedProductApi()->all($pageSize, $queryParams), $this->client, $this->logger, $this->attributes, @@ -229,15 +230,66 @@ public function getProducts(int $pageSize, ?\DateTime $updatedAt = null) } return new ProductIterator( - $this->client->getProductApi()->all( - $pageSize, - ['search' => $searchFilters, 'scope' => $this->transportEntity->getAkeneoActiveChannel()] - ), + $this->client->getProductApi()->all($pageSize, $queryParams), $this->client, $this->logger, $this->attributes, $this->familyVariants, $this->measureFamilies, + $this->getAttributeMapping(), + $this->getAlternativeIdentifier() + + ); + } + + /** + * {@inheritdoc} + * + * @return \Iterator + */ + public function getProductsForVariants(int $pageSize, ?\DateTime $updatedAt = null): iterable + { + $queryParams = [ + 'scope' => $this->transportEntity->getAkeneoActiveChannel(), + 'search' => $this->akeneoSearchBuilder->getFilters((new ParseUpdatedPlaceholder($this->transportEntity->getProductFilter(), $updatedAt))()), + ]; + + return new ProductIterator( + $this->client->getProductApi()->all($pageSize, $queryParams), + $this->client, + $this->logger, + $this->attributes, + $this->familyVariants, + $this->measureFamilies, + $this->getAttributeMapping(), + $this->getAlternativeIdentifier() + + ); + } + + public function getProductsList(int $pageSize, ?\DateTime $updatedAt = null): iterable + { + $this->initAttributesList(); + + $queryParams = [ + 'scope' => $this->transportEntity->getAkeneoActiveChannel(), + 'search' => $this->akeneoSearchBuilder->getFilters((new ParseUpdatedPlaceholder($this->transportEntity->getProductFilter(), $updatedAt))()), + 'attributes' => key($this->attributes), + ]; + + if ($this->transportEntity->getSyncProducts() === SyncProductsDataProvider::PUBLISHED) { + return new ConfigurableProductIterator( + $this->client->getPublishedProductApi()->all($pageSize, $queryParams), + $this->client, + $this->logger, + $this->getAttributeMapping() + ); + } + + return new ConfigurableProductIterator( + $this->client->getProductApi()->all($pageSize, $queryParams), + $this->client, + $this->logger, $this->getAttributeMapping() ); } @@ -251,16 +303,13 @@ public function getProductModels(int $pageSize, ?\DateTime $updatedAt = null) $this->initFamilyVariants(); $this->initMeasureFamilies(); - $searchFilters = $this->akeneoSearchBuilder->getFilters((new ParseUpdatedPlaceholder($this->transportEntity->getProductFilter(), $updatedAt))()); - if (isset($searchFilters['completeness'])) { - unset($searchFilters['completeness']); - } + $queryParams = [ + 'scope' => $this->transportEntity->getAkeneoActiveChannel(), + 'search' => $this->akeneoSearchBuilder->getFilters((new ParseUpdatedPlaceholder($this->transportEntity->getConfigurableProductFilter(), $updatedAt))()), + ]; return new ProductIterator( - $this->client->getProductModelApi()->all( - $pageSize, - ['search' => $searchFilters, 'scope' => $this->transportEntity->getAkeneoActiveChannel()] - ), + $this->client->getProductModelApi()->all($pageSize, $queryParams), $this->client, $this->logger, $this->attributes, @@ -270,6 +319,24 @@ public function getProductModels(int $pageSize, ?\DateTime $updatedAt = null) ); } + public function getProductModelsList(int $pageSize, ?\DateTime $updatedAt = null): iterable + { + $this->initAttributesList(); + + $queryParams = [ + 'scope' => $this->transportEntity->getAkeneoActiveChannel(), + 'search' => $this->akeneoSearchBuilder->getFilters((new ParseUpdatedPlaceholder($this->transportEntity->getConfigurableProductFilter(), $updatedAt))()), + 'attributes' => key($this->attributes), + ]; + + return new ConfigurableProductIterator( + $this->client->getProductModelApi()->all($pageSize, $queryParams), + $this->client, + $this->logger, + $this->getAttributeMapping() + ); + } + /** * {@inheritdoc} */ @@ -515,7 +582,7 @@ protected function initMeasureFamilies() } } - protected function getAttributeMapping(): array + public function getAttributeMapping(): array { if ($this->attributeMapping) { return $this->attributeMapping; @@ -560,8 +627,8 @@ public function getBrands(): \Traversable } } - private function getAlternativeIdentifier(): ?string - { + public function getAlternativeIdentifier(): ?string + { return $this->transportEntity->getAlternativeIdentifier(); - } + } } diff --git a/Integration/AkeneoTransportInterface.php b/Integration/AkeneoTransportInterface.php index 409915b0..a31d208a 100644 --- a/Integration/AkeneoTransportInterface.php +++ b/Integration/AkeneoTransportInterface.php @@ -46,11 +46,22 @@ public function getAttributeFamilies(); */ public function getProducts(int $pageSize, ?\DateTime $updatedAt = null); + /** + * {@inheritdoc} + * + * @return \Iterator + */ + public function getProductsForVariants(int $pageSize, ?\DateTime $updatedAt = null); + /** * @return \Iterator */ public function getProductModels(int $pageSize, ?\DateTime $updatedAt = null); + public function getProductsList(int $pageSize): iterable; + + public function getProductModelsList(int $pageSize): iterable; + /** * @return \Iterator */ @@ -65,4 +76,6 @@ public function downloadAndSaveAsset(string $code, string $file): void; public function downloadAndSaveReferenceEntityMediaFile(string $code): void; public function downloadAndSaveAssetMediaFile(string $code): void; + + public function getAlternativeIdentifier(): ?string; } diff --git a/Integration/Connector/ConfigurableProductConnector.php b/Integration/Connector/ConfigurableProductConnector.php new file mode 100644 index 00000000..dd77c9b9 --- /dev/null +++ b/Integration/Connector/ConfigurableProductConnector.php @@ -0,0 +1,70 @@ +schemaUpdateFilter = $schemaUpdateFilter; + } + + public function isAllowed(Channel $integration, array $processedConnectorsStatuses): bool + { + return $this->schemaUpdateFilter->isApplicable($integration, Product::class) === false; + } + + protected function getConnectorSource() + { + $variants = $this->cacheProvider->fetch('akeneo')['variants'] ?? []; + if ($variants) { + return new \ArrayIterator(); + } + + $iterator = new \AppendIterator(); + $iterator->append($this->transport->getProductsList(self::PAGE_SIZE,$this->getLastSyncDate())); + $iterator->append($this->transport->getProductModelsList(self::PAGE_SIZE,$this->getLastSyncDate())); + + return $iterator; + } +} diff --git a/Integration/Connector/ProductConnector.php b/Integration/Connector/ProductConnector.php index e3ddf4da..c1af853a 100644 --- a/Integration/Connector/ProductConnector.php +++ b/Integration/Connector/ProductConnector.php @@ -7,14 +7,13 @@ use Oro\Bundle\ImportExportBundle\Context\ContextInterface; use Oro\Bundle\IntegrationBundle\Entity\Channel; use Oro\Bundle\IntegrationBundle\Provider\AllowedConnectorInterface; -use Oro\Bundle\IntegrationBundle\Provider\ConnectorInterface; use Oro\Bundle\ProductBundle\Entity\Product; use Psr\Log\LoggerAwareInterface; /** * Integration product connector. */ -class ProductConnector extends AbstractOroAkeneoConnector implements ConnectorInterface, AllowedConnectorInterface +class ProductConnector extends AbstractOroAkeneoConnector implements AllowedConnectorInterface { use CacheProviderTrait; @@ -78,17 +77,10 @@ public function setSchemaUpdateFilter(SchemaUpdateFilter $schemaUpdateFilter): v protected function getConnectorSource() { $items = $this->cacheProvider->fetch('akeneo')['items'] ?? []; - if ($items) { return new \ArrayIterator(); } - $variants = $this->cacheProvider->fetch('akeneo')['variants'] ?? []; - - if ($variants) { - return new \ArrayIterator(); - } - $iterator = new \AppendIterator(); $iterator->append($this->transport->getProducts(self::PAGE_SIZE, $this->getLastSyncDate())); $iterator->append($this->transport->getProductModels(self::PAGE_SIZE, $this->getLastSyncDate())); diff --git a/Integration/Connector/VariantProductConnector.php b/Integration/Connector/VariantProductConnector.php new file mode 100644 index 00000000..d59690c1 --- /dev/null +++ b/Integration/Connector/VariantProductConnector.php @@ -0,0 +1,97 @@ +needToUpdateSchema($integration) && $integration->getTransport()->getSyncProducts() === SyncProductsDataProvider::PUBLISHED; + } + + public function setSchemaUpdateFilter(SchemaUpdateFilter $schemaUpdateFilter): void + { + $this->schemaUpdateFilter = $schemaUpdateFilter; + } + + /** + * {@inheritdoc} + */ + protected function getConnectorSource() + { + $iterator = new \AppendIterator(); + $iterator->append($this->transport->getProductsForVariants(self::PAGE_SIZE, $this->getLastSyncDate())); + + return $iterator; + } + + /** + * Checks if schema is changed and need to update it. + */ + private function needToUpdateSchema(Channel $integration): bool + { + return $this->schemaUpdateFilter->isApplicable($integration, Product::class); + } + + public function getOrder() + { + return 7; + } +} diff --git a/Integration/Iterator/ConfigurableProductIterator.php b/Integration/Iterator/ConfigurableProductIterator.php new file mode 100644 index 00000000..4a533506 --- /dev/null +++ b/Integration/Iterator/ConfigurableProductIterator.php @@ -0,0 +1,38 @@ +attributeMapping = $attributeMapping; + } + + public function doCurrent() + { + $item = $this->resourceCursor->current(); + + $sku = $item['identifier'] ?? $item['code']; + + if (array_key_exists('sku', $this->attributeMapping)) { + if (!empty($item['values'][$this->attributeMapping['sku']][0]['data'])) { + $sku = $item['values'][$this->attributeMapping['sku']][0]['data']; + } + } + + return ['sku' => (string)$sku, 'parent' => $item['parent'] ?? null, 'family_variant' => $item['family_variant'] ?? null]; + } +} diff --git a/Job/Context/SimpleContextAggregator.php b/Job/Context/SimpleContextAggregator.php index c98e8a7e..869bdc7f 100644 --- a/Job/Context/SimpleContextAggregator.php +++ b/Job/Context/SimpleContextAggregator.php @@ -25,9 +25,9 @@ public function getAggregatedContext(JobExecution $jobExecution) $context, $this->contextRegistry->getByStepExecution($stepExecution) ); - //CUSTOMIZATION START + // CUSTOMIZATION START $context->addErrors($stepExecution->getErrors()); - //CUSTOMIZATION END + // CUSTOMIZATION END } } diff --git a/Migrations/Schema/OroAkeneoBundleInstaller.php b/Migrations/Schema/OroAkeneoBundleInstaller.php index 3938e0e5..2e318fd7 100644 --- a/Migrations/Schema/OroAkeneoBundleInstaller.php +++ b/Migrations/Schema/OroAkeneoBundleInstaller.php @@ -47,7 +47,7 @@ class OroAkeneoBundleInstaller implements Installation, ExtendExtensionAwareInte */ public function getMigrationVersion() { - return 'v1_15'; + return 'v1_16'; } /** @@ -113,6 +113,7 @@ protected function updateIntegrationTransportTable(Schema $schema) $table->addColumn('akeneo_active_channel', 'string', ['notnull' => false, 'length' => 255]); $table->addColumn('akeneo_acl_voter_enabled', 'boolean', ['notnull' => false]); $table->addColumn('akeneo_product_filter', 'text', ['notnull' => false]); + $table->addColumn('akeneo_conf_product_filter', 'text', ['notnull' => false]); $table->addColumn('akeneo_attributes_list', 'text', ['notnull' => false]); $table->addColumn('rootcategory_id', 'integer', ['notnull' => false]); $table->addColumn('pricelist_id', 'integer', ['notnull' => false]); diff --git a/Migrations/Schema/v1_16/OroAkeneoMigration.php b/Migrations/Schema/v1_16/OroAkeneoMigration.php new file mode 100644 index 00000000..8c1a2368 --- /dev/null +++ b/Migrations/Schema/v1_16/OroAkeneoMigration.php @@ -0,0 +1,19 @@ +getTable('oro_integration_transport'); + $table->addColumn('akeneo_conf_product_filter', 'text', ['notnull' => false]); + + $queries->addPostQuery('UPDATE oro_integration_transport SET akeneo_conf_product_filter = akeneo_product_filter ' . + 'WHERE akeneo_product_filter IS NOT NULL;'); + } +} diff --git a/README.md b/README.md index 030d7c58..3607b462 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # Akeneo PIM OroCommerce Connector ## Short overview @@ -24,6 +23,7 @@ With this extension, you will be able to sync the following data from Akeneo to | 4.1 | EOL | 4.1 | 2.3, 3.2, 4.0* | | 4.2 | 2022 | 4.2 | 3.2, 4.0, 5.0 | | 5.0 | 2023 | 5.0 | 5.0+ | + ** Akeneo supported using older client versions, new features are not available.** ## Installation diff --git a/Resources/config/batch_jobs.yml b/Resources/config/batch_jobs.yml index 75fc8a5b..81d09fe6 100644 --- a/Resources/config/batch_jobs.yml +++ b/Resources/config/batch_jobs.yml @@ -67,7 +67,7 @@ connector: class: Oro\Bundle\BatchBundle\Step\ItemStep services: reader: oro_akeneo.integration.connector.product - processor: oro_akeneo.importexport.processor.async + processor: oro_akeneo.importexport.processor.build_variant_cache writer: oro_akeneo.importexport.writer.async_product parameters: batch_size: 25 @@ -92,9 +92,23 @@ connector: reader: oro_akeneo.importexport.reader.price processor: oro_akeneo.importexport.processor.import.product_price writer: oro_pricing.importexport.writer.product_price - import_variants: + + akeneo_configurable_product_import: + title: "Configurable Product import from Akeneo" + type: import + steps: + api: title: import class: Oro\Bundle\BatchBundle\Step\ItemStep + services: + reader: oro_akeneo.integration.connector.configurable_product + processor: oro_akeneo.importexport.processor.async + writer: oro_akeneo.importexport.writer.configurable_async_product + parameters: + batch_size: 25 + import_variants: + title: import + class: Oro\Bundle\AkeneoBundle\ImportExport\Step\ItemStep services: reader: oro_akeneo.importexport.reader.product_variant processor: oro_akeneo.importexport.processor.import.product_variant @@ -102,6 +116,29 @@ connector: parameters: batch_size: 1 + akeneo_variant_product_import: + title: "Link Product variant to configurable from Akeneo" + type: import + steps: + build_cache_variants: + title: import + class: Oro\Bundle\BatchBundle\Step\ItemStep + services: + reader: oro_akeneo.integration.connector.variant + processor: oro_akeneo.importexport.processor.build_variant_cache + writer: oro_akeneo.importexport.writer.empty + parameters: + batch_size: 25 + save_variants: + title: import + class: Oro\Bundle\BatchBundle\Step\ItemStep + services: + reader: oro_akeneo.importexport.reader.product_variant + processor: oro_akeneo.importexport.processor.import.product_variant + writer: oro_integration.writer.persistent_batch_writer + parameters: + batch_size: 25 + akeneo_brand_import: title: "Brand import from Akeneo" type: import diff --git a/Resources/config/commands.yml b/Resources/config/commands.yml index b6bd5980..60d55f6d 100644 --- a/Resources/config/commands.yml +++ b/Resources/config/commands.yml @@ -1,9 +1,9 @@ services: - _defaults: - public: false + _defaults: + public: false - Oro\Bundle\AkeneoBundle\Command\CleanupCommand: - arguments: - - '@oro_entity.doctrine_helper' - tags: - - { name: console.command } + Oro\Bundle\AkeneoBundle\Command\CleanupCommand: + arguments: + - '@oro_entity.doctrine_helper' + tags: + - { name: console.command } diff --git a/Resources/config/controllers.yml b/Resources/config/controllers.yml index 40418c37..7781e9b5 100644 --- a/Resources/config/controllers.yml +++ b/Resources/config/controllers.yml @@ -1,13 +1,13 @@ services: - _defaults: - public: true + _defaults: + public: true - Oro\Bundle\AkeneoBundle\Controller\ValidateConnectionController: - arguments: - - '@oro_currency.config.currency' - - '@translator' - - '@oro_akeneo.integration.transport' - calls: - - [setContainer, ['@Psr\Container\ContainerInterface']] - tags: - - { name: container.service_subscriber } + Oro\Bundle\AkeneoBundle\Controller\ValidateConnectionController: + arguments: + - '@oro_currency.config.currency' + - '@translator' + - '@oro_akeneo.integration.transport' + calls: + - [setContainer, ['@Psr\Container\ContainerInterface']] + tags: + - { name: container.service_subscriber } diff --git a/Resources/config/importexport.yml b/Resources/config/importexport.yml index 66da8f67..81787301 100644 --- a/Resources/config/importexport.yml +++ b/Resources/config/importexport.yml @@ -106,6 +106,10 @@ services: oro_akeneo.importexport.processor.async: class: 'Oro\Bundle\AkeneoBundle\ImportExport\Processor\AsyncProcessor' public: true + + oro_akeneo.importexport.processor.build_variant_cache: + class: 'Oro\Bundle\AkeneoBundle\ImportExport\Processor\BuildVariantCacheProcessor' + public: true calls: - [ setCacheProvider, [ '@oro_akeneo.importexport.cache' ] ] @@ -141,6 +145,7 @@ services: index_1: '@oro_akeneo.strategy.import.helper' calls: - [ setConfigManager, [ '@oro_entity_config.config_manager' ] ] + oro_akeneo.importexport.processor.attribute: class: 'Oro\Bundle\AkeneoBundle\ImportExport\Processor\AttributeImportProcessor' public: true @@ -204,6 +209,15 @@ services: calls: - [ setCacheProvider, [ '@oro_akeneo.importexport.cache' ] ] + oro_akeneo.integration.connector.brand: + class: 'Oro\Bundle\AkeneoBundle\Integration\Connector\BrandConnector' + arguments: + - '@oro_importexport.context_registry' + - '@oro_integration.logger.strategy' + - '@oro_integration.provider.connector_context_mediator' + tags: + - { name: oro_integration.connector, type: brand, channel_type: oro_akeneo } + oro_akeneo.integration.connector.product: class: 'Oro\Bundle\AkeneoBundle\Integration\Connector\ProductConnector' arguments: @@ -212,19 +226,36 @@ services: - '@oro_integration.provider.connector_context_mediator' calls: - [ setManagerRegistry, [ "@doctrine" ] ] - - [ setCacheProvider, [ '@oro_akeneo.importexport.cache' ] ] - [ setSchemaUpdateFilter, [ '@oro_akeneo.placeholder.schema_update_filter' ] ] + - [ setCacheProvider, [ '@oro_akeneo.importexport.cache' ] ] tags: - { name: oro_integration.connector, type: product, channel_type: oro_akeneo } - oro_akeneo.integration.connector.brand: - class: 'Oro\Bundle\AkeneoBundle\Integration\Connector\BrandConnector' + + oro_akeneo.integration.connector.configurable_product: + class: 'Oro\Bundle\AkeneoBundle\Integration\Connector\ConfigurableProductConnector' arguments: - '@oro_importexport.context_registry' - '@oro_integration.logger.strategy' - '@oro_integration.provider.connector_context_mediator' + calls: + - [ setSchemaUpdateFilter, [ '@oro_akeneo.placeholder.schema_update_filter' ] ] + - [ setCacheProvider, [ '@oro_akeneo.importexport.cache' ] ] tags: - - { name: oro_integration.connector, type: brand, channel_type: oro_akeneo } + - { name: oro_integration.connector, type: configurable_product, channel_type: oro_akeneo } + + oro_akeneo.integration.connector.variant: + class: 'Oro\Bundle\AkeneoBundle\Integration\Connector\VariantProductConnector' + arguments: + - '@oro_importexport.context_registry' + - '@oro_integration.logger.strategy' + - '@oro_integration.provider.connector_context_mediator' + calls: + - [ setManagerRegistry, [ "@doctrine" ] ] + - [ setSchemaUpdateFilter, [ '@oro_akeneo.placeholder.schema_update_filter' ] ] + - [ setCacheProvider, [ '@oro_akeneo.importexport.cache' ] ] + tags: + - { name: oro_integration.connector, type: product_variant, channel_type: oro_akeneo } oro_akeneo.importexport.data_converter.product: class: 'Oro\Bundle\AkeneoBundle\ImportExport\DataConverter\ProductDataConverter' @@ -233,10 +264,11 @@ services: - [ setTranslateUsingLocale, [ false ] ] - [ setEntityConfigManager, [ '@oro_entity_config.config_manager' ] ] - [ setDateTimeFormatter, [ '@oro_locale.formatter.date_time' ] ] + - [ setProductUnitsProvider, [ '@oro_product.provider.product_units_provider' ] ] - [ setDoctrineHelper, [ '@oro_entity.doctrine_helper' ] ] - [ setProductVariantFieldValueHandlerRegistry, [ '@oro_product.product_variant_field.registry.product_variant_field_value_handler_registry' ] ] - [setProductUnitDiscovery, ["@oro_akeneo.product_unit.discovery"]] - - [setLogger, ["@oro_integration.logger.strategy"]] - - [setCodePrefix, ['%oro_akeneo.importexport.code_prefix%']] + - [ setLogger, ["@oro_integration.logger.strategy"]] + - [ setCodePrefix, ['%oro_akeneo.importexport.code_prefix%']] oro_akeneo.importexport.strategy.product: class: 'Oro\Bundle\AkeneoBundle\ImportExport\Strategy\ProductImportStrategy' parent: oro_product.importexport.strategy.product @@ -318,12 +350,22 @@ services: arguments: - '@oro_message_queue.message_producer' - '@oro_entity.doctrine_helper' - calls: - - [ setCacheProvider, [ '@oro_akeneo.importexport.cache' ] ] + - '@oro_platform.optional_listeners.manager' + - '@oro_akeneo.event_listener.additional_optional_listeners_manager' + + oro_akeneo.importexport.writer.empty: + class: 'Oro\Bundle\AkeneoBundle\ImportExport\Writer\EmptyWriter' + + oro_akeneo.importexport.writer.configurable_async_product: + class: 'Oro\Bundle\AkeneoBundle\ImportExport\Writer\ConfigurableAsyncWriter' + arguments: + - '@oro_message_queue.message_producer' + - '@oro_entity.doctrine_helper' + - '@oro_platform.optional_listeners.manager' + - '@oro_akeneo.event_listener.additional_optional_listeners_manager' oro_akeneo.importexport.cache: - parent: oro_cache.array_cache - public: false + class: Doctrine\Common\Cache\ArrayCache calls: - [ setNamespace, [ 'oro_akeneo' ] ] diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 05bdbbb4..abfd9edf 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -404,13 +404,13 @@ services: decorates: oro_serialized_fields.event_listener.deleted_attribute_relation_serialized parent: oro_serialized_fields.event_listener.deleted_attribute_relation_serialized + oro_akeneo.entity_config.importexport_field_configuration: + class: Oro\Bundle\AkeneoBundle\EntityConfig\ImportexportFieldConfiguration + tags: + - oro_entity_config.validation.entity_config + oro_akeneo.product_unit.discovery: class: Oro\Bundle\AkeneoBundle\ProductUnit\ImportFromAkeneoDiscovery arguments: - '@oro_config.manager' - '@oro_product.provider.product_units_provider' - - oro_akeneo.entity_config.importexport_field_configuration: - class: Oro\Bundle\AkeneoBundle\EntityConfig\ImportexportFieldConfiguration - tags: - - oro_entity_config.validation.entity_config diff --git a/Resources/translations/messages.en.yml b/Resources/translations/messages.en.yml index cf5e0967..4f05dc3b 100644 --- a/Resources/translations/messages.en.yml +++ b/Resources/translations/messages.en.yml @@ -38,6 +38,9 @@ oro: akeneo_product_filter: label: 'Product Filter' tooltip: 'This field enables you to apply filters to sync only the products you want. As this filter is passed via API request, it must be filled in JSON format. Details on the format and filter options available for the products can be found in the Filters section of the Akeneo PIM documentation' + akeneo_configurable_product_filter: + label: 'Configurable Product Filter' + tooltip: 'This field enables you to apply filters to sync only the configurable products you want. As this filter is passed via API request, it must be filled in JSON format. Details on the format and filter options available for the products can be found in the Filters section of the Akeneo PIM documentation' akeneo_attribute_list: label: 'Attribute Filter' tooltip: 'This field enables you to apply filters to sync only the attributes you want. Values must be attribute code, separated with a semi-colon. IMPORTANT: if not defined before to save the integration, all attributes will be imported.' @@ -87,6 +90,10 @@ oro: label: Category connector product: label: Product connector + product_variant: + label: Product variant connector + configurable_product: + label: Configurable product connector attribute_family: label: Attribute family connector attribute: diff --git a/Resources/views/Datagrid/attributeFamilies.html.twig b/Resources/views/Datagrid/attributeFamilies.html.twig index 100a26d0..b329b0c0 100644 --- a/Resources/views/Datagrid/attributeFamilies.html.twig +++ b/Resources/views/Datagrid/attributeFamilies.html.twig @@ -1,4 +1,3 @@ -{% import '@OroEntityConfig/macros.html.twig' as entityConfig %} {% import '@OroUI/macros.html.twig' as UI %} {% if is_granted('oro_attribute_family_view') %} diff --git a/Resources/views/Form/fields.html.twig b/Resources/views/Form/fields.html.twig index 5cb58f93..408bcf1d 100644 --- a/Resources/views/Form/fields.html.twig +++ b/Resources/views/Form/fields.html.twig @@ -194,6 +194,17 @@ +
+
+ {{ UI.tooltip('oro.akeneo.integration.settings.akeneo_configurable_product_filter.tooltip'|trans, {}, 'right') }} + {{ form_label(form.configurableProductFilter) }} +
+
+ {{ form_widget(form.configurableProductFilter) }} + {{ form_errors(form.configurableProductFilter) }} +
+
+
{{ UI.tooltip('oro.akeneo.integration.settings.akeneo_attribute_list.tooltip'|trans, {}, 'right') }} diff --git a/Validator/UniqueProductVariantLinksValidator.php b/Validator/UniqueProductVariantLinksValidator.php index e6e8b001..fa0ded4b 100644 --- a/Validator/UniqueProductVariantLinksValidator.php +++ b/Validator/UniqueProductVariantLinksValidator.php @@ -2,8 +2,8 @@ namespace Oro\Bundle\AkeneoBundle\Validator; +use Doctrine\ORM\PersistentCollection; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; -use Oro\Bundle\ProductBundle\Entity\Product; use Oro\Bundle\ProductBundle\Validator\Constraints\ConfigurableProductAccessorTrait; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -40,17 +40,14 @@ public function validate($value, Constraint $constraint) return; } if (count($product->getVariantFields()) === 0) { - return null; + return; } - $uow = $this->doctrineHelper->getEntityManagerForClass(Product::class)->getUnitOfWork(); - $collections = array_merge($uow->getScheduledCollectionUpdates(), $uow->getScheduledCollectionDeletions()); - if ( - !in_array($value->getVariantLinks(), $collections) - && !in_array($value->getParentVariantLinks(), $collections) - && empty($uow->getEntityChangeSet($value)['variantFields']) - ) { - return; + $variantLinks = $value->getVariantLinks(); + if ($variantLinks instanceof PersistentCollection) { + if ($variantLinks->isInitialized() && !$variantLinks->isDirty()) { + return; + } } $this->validator->validate($value, $constraint);