Commit 305f3c89 authored by Piotr Borek's avatar Piotr Borek
Browse files

#767 Copy products into family variants

parent f802fc6d
......@@ -19,14 +19,14 @@ use Oro\Bundle\PimDataGridBundle\Extension\Pager\PagerExtension;
use PcmtDraftBundle\Datasource\OriginalAssociatedProductDatasource;
use PcmtDraftBundle\Entity\AbstractDraft;
use PcmtDraftBundle\Repository\DraftRepository;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\DatagridBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ExistingProductDraftBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductModelAssociationBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
......
......@@ -19,14 +19,14 @@ use Oro\Bundle\PimDataGridBundle\Extension\Pager\PagerExtension;
use PcmtDraftBundle\Datasource\OriginalAssociatedProductModelDatasource;
use PcmtDraftBundle\Entity\AbstractDraft;
use PcmtDraftBundle\Repository\DraftRepository;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\DatagridBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ExistingProductDraftBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductModelAssociationBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
......
......@@ -17,10 +17,10 @@ use Akeneo\Tool\Component\StorageUtils\Saver\SaverInterface;
use PcmtDraftBundle\Repository\DraftRepository;
use PcmtDraftBundle\Service\Associations\AssociationThroughDraftAdding;
use PcmtDraftBundle\Service\Draft\GeneralObjectFromDraftCreator;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ExistingProductDraftBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
......
......@@ -18,13 +18,13 @@ use Akeneo\Tool\Component\StorageUtils\Saver\SaverInterface;
use PcmtDraftBundle\Repository\DraftRepository;
use PcmtDraftBundle\Service\Associations\AssociationThroughDraftRemoving;
use PcmtDraftBundle\Service\Draft\GeneralObjectFromDraftCreator;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ExistingProductDraftBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductModelAssociationBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
......
......@@ -16,13 +16,13 @@ use PcmtDraftBundle\Service\Associations\AssociationThroughDraftAdding;
use PcmtDraftBundle\Service\Associations\AssociationThroughDraftRemoving;
use PcmtDraftBundle\Service\Associations\BiDirectionalAssociationUpdater;
use PcmtDraftBundle\Service\Draft\GeneralObjectFromDraftCreator;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ExistingProductDraftBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductModelBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductModelAssociationBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
......
......@@ -14,11 +14,11 @@ use Doctrine\ORM\EntityManagerInterface;
use PcmtDraftBundle\Service\AttributeChange\AttributeChangeService;
use PcmtDraftBundle\Service\Draft\ChangesChecker;
use PcmtDraftBundle\Service\Draft\GeneralObjectFromDraftCreator;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ExistingProductDraftBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PcmtDraftBundle\Tests\TestDataBuilder\ProductBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationCollectionBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\AssociationTypeBuilder;
use PcmtSharedBundle\Tests\TestDataBuilder\ProductAssociationBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
......
......@@ -9,8 +9,12 @@ declare(strict_types=1);
namespace PcmtRulesBundle\Connector\Job\Step;
use Akeneo\Pim\Enrichment\Component\Product\Query\Filter\Operators;
use Akeneo\Pim\Enrichment\Component\Product\Query\ProductQueryBuilderFactoryInterface;
use Akeneo\Pim\Structure\Component\Repository\FamilyRepositoryInterface;
use Akeneo\Tool\Component\Batch\Model\StepExecution;
use Akeneo\Tool\Component\Batch\Step\AbstractStep;
use PcmtRulesBundle\Service\CopyProductsRule\CopyProductsRuleProcessor;
class CopyProductsRuleStep extends AbstractStep
{
......@@ -18,6 +22,30 @@ class CopyProductsRuleStep extends AbstractStep
public const PARAM_DESTINATION_FAMILY = 'destinationFamily';
/** @var FamilyRepositoryInterface */
private $familyRepository;
/** @var ProductQueryBuilderFactoryInterface */
private $pqbFactory;
/** @var CopyProductsRuleProcessor */
private $productProcessor;
public function setFamilyRepository(FamilyRepositoryInterface $familyRepository): void
{
$this->familyRepository = $familyRepository;
}
public function setPqbFactory(ProductQueryBuilderFactoryInterface $pqbFactory): void
{
$this->pqbFactory = $pqbFactory;
}
public function setProductProcessor(CopyProductsRuleProcessor $productProcessor): void
{
$this->productProcessor = $productProcessor;
}
protected function doExecute(StepExecution $stepExecution): void
{
$jobParameters = $stepExecution->getJobParameters();
......@@ -27,5 +55,31 @@ class CopyProductsRuleStep extends AbstractStep
$text[] = $key.' : '. $value;
}
$stepExecution->addSummaryInfo('parameters', implode(', ', $text));
$sourceFamilyCode = $jobParameters->get(self::PARAM_SOURCE_FAMILY);
$destinationFamilyCode = $jobParameters->get(self::PARAM_DESTINATION_FAMILY);
$sourceFamily = $this->familyRepository->findOneBy(['code' => $sourceFamilyCode]);
$destinationFamily = $this->familyRepository->findOneBy(['code' => $destinationFamilyCode]);
if (!$sourceFamily || !$destinationFamily) {
return;
}
$stepExecution->addSummaryInfo('sf', $sourceFamily->getCode());
$stepExecution->addSummaryInfo('df', $destinationFamily->getCode());
$pqb = $this->pqbFactory->create([
'default_locale' => null,
'default_scope' => null,
'limit' => 100000,
]);
$pqb->addFilter('family', Operators::IN_LIST, [$sourceFamily->getCode()]);
$entityCursor = $pqb->execute();
foreach ($entityCursor as $product) {
$stepExecution->incrementSummaryInfo('source_products_found', 1);
$this->productProcessor->process($stepExecution, $destinationFamily, $product);
}
}
}
\ No newline at end of file
......@@ -85,6 +85,24 @@ services:
pcmt_rules.service.image_verification_service:
class: PcmtRulesBundle\Service\ImageVerificationService
pcmt_rules.service.copy_products_rule.copy_products_rule_processor:
class: PcmtRulesBundle\Service\CopyProductsRule\CopyProductsRuleProcessor
arguments:
- '@pcmt_rules.service.copy_products_rule.copy_product_to_product_model'
pcmt_rules.service.copy_products_rule.copy_product_to_product_model:
class: PcmtRulesBundle\Service\CopyProductsRule\CopyProductToProductModel
arguments:
- '@pcmt.service.rule_attribute_provider'
- '@pim_catalog.saver.product'
- '@pim_catalog.saver.product_model'
- '@pim_catalog.builder.product'
- '@pcmt.service.rule_processor_copier'
- '@pcmt_rules.service.copy_products_rule.sub_entity_finder'
pcmt_rules.service.copy_products_rule.sub_entity_finder:
class: PcmtRulesBundle\Service\CopyProductsRule\SubEntityFinder
pcmt.constraints.attribute_exists_in_both_families_constraint_validator:
class: PcmtRulesBundle\Validators\AttributeExistsInBothFamiliesConstraintValidator
arguments:
......
......@@ -6,6 +6,10 @@ services:
- 'copy_products_rule_step'
- '@event_dispatcher'
- '@akeneo_batch.job_repository'
calls:
- [setFamilyRepository, ['@pim_catalog.repository.family']]
- [setPqbFactory, ['@pim_enrich.query.product_and_product_model_query_builder_from_size_factory']]
- [setProductProcessor, ['@pcmt_rules.service.copy_products_rule.copy_products_rule_processor']]
pcmt.connector.job.step.family_to_family:
class: PcmtRulesBundle\Connector\Job\Step\FamilyToFamilyStep
......@@ -45,3 +49,4 @@ services:
<?php
/*
* Copyright (c) 2021, VillageReach
* Licensed under the Non-Profit Open Software License version 3.0.
* SPDX-License-Identifier: NPOSL-3.0
*/
declare(strict_types=1);
/*
* Copyright (c) 2021, VillageReach
* Licensed under the Non-Profit Open Software License version 3.0.
* SPDX-License-Identifier: NPOSL-3.0
*/
namespace PcmtRulesBundle\Service\CopyProductsRule;
use Akeneo\Pim\Enrichment\Component\Product\Builder\ProductBuilderInterface;
use Akeneo\Pim\Enrichment\Component\Product\Model\EntityWithValuesInterface;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductInterface;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductModel;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductModelInterface;
use Akeneo\Pim\Structure\Component\Model\AttributeInterface;
use Akeneo\Pim\Structure\Component\Model\VariantAttributeSetInterface;
use Akeneo\Tool\Component\Batch\Model\StepExecution;
use Akeneo\Tool\Component\StorageUtils\Saver\SaverInterface;
use PcmtRulesBundle\Service\RuleAttributeProvider;
use PcmtRulesBundle\Service\RuleProcessorCopier;
use Ramsey\Uuid\Uuid;
class CopyProductToProductModel
{
/** @var StepExecution */
private $stepExecution;
/** @var RuleAttributeProvider */
private $ruleAttributeProvider;
/** @var SaverInterface */
private $productSaver;
/** @var ProductBuilderInterface */
private $variantProductBuilder;
/** @var RuleProcessorCopier */
private $ruleProcessorCopier;
/** @var SaverInterface */
private $productModelSaver;
/** @var SubEntityFinder */
private $subEntityFinder;
public function __construct(
RuleAttributeProvider $ruleAttributeProvider,
SaverInterface $productSaver,
SaverInterface $productModelSaver,
ProductBuilderInterface $variantProductBuilder,
RuleProcessorCopier $ruleProcessorCopier,
SubEntityFinder $subEntityFinder
) {
$this->ruleAttributeProvider = $ruleAttributeProvider;
$this->productSaver = $productSaver;
$this->productModelSaver = $productModelSaver;
$this->variantProductBuilder = $variantProductBuilder;
$this->ruleProcessorCopier = $ruleProcessorCopier;
$this->subEntityFinder = $subEntityFinder;
}
public function process(
StepExecution $stepExecution,
ProductInterface $sourceProduct,
ProductModelInterface $destinationProductModel
): void {
$this->stepExecution = $stepExecution;
$this->processDestinationProductModel($sourceProduct, $destinationProductModel, 1);
}
private function processDestinationProductModel(
ProductInterface $sourceProduct,
ProductModelInterface $destinationProductModel,
int $level
): void {
$variant = $destinationProductModel->getFamilyVariant();
$set = $variant->getVariantAttributeSet($level);
/** @var VariantAttributeSetInterface $set */
$axisAttributes = $set->getAxes();
foreach ($axisAttributes as $attribute) {
/** @var AttributeInterface $attribute */
$value = $sourceProduct->getValue($attribute->getCode());
if (!$value || !$value->getData()) {
$this->stepExecution->incrementSummaryInfo('axis_attribute_not_exists_in_source_product', 1);
return;
}
}
$attributes = $this->ruleAttributeProvider->getAllForFamilies(
$sourceProduct->getFamily(),
$destinationProductModel->getFamily()
);
foreach ($attributes as $key => $attribute) {
if (!$set->hasAttribute($attribute)) {
unset($attributes[$key]);
}
}
$subEntity = $this->subEntityFinder->findByAxisAttributes($destinationProductModel, $axisAttributes, $sourceProduct);
if ($subEntity) {
$this->stepExecution->incrementSummaryInfo('subentities_found', 1);
$this->copy($sourceProduct, $subEntity, $attributes);
if ($subEntity instanceof ProductModelInterface) {
$this->processDestinationProductModel($sourceProduct, $subEntity, ++$level);
}
} else {
if ($level < $variant->getNumberOfLevel()) {
$subProductModel = $this->createNewSubProductModel(
$sourceProduct,
$destinationProductModel,
$attributes
);
$this->stepExecution->incrementSummaryInfo('sub_product_models_created', 1);
$this->processDestinationProductModel($sourceProduct, $subProductModel, ++$level);
} else {
$this->createNewSubProduct(
$sourceProduct,
$destinationProductModel,
$attributes
);
$this->stepExecution->incrementSummaryInfo('sub_products_created', 1);
}
}
}
private function copy(
ProductInterface $sourceProduct,
EntityWithValuesInterface $destinationEntity,
array $attributes
): void {
foreach ($attributes as $key => $attribute) {
/** @var AttributeInterface $attribute */
if (!$sourceProduct->getValue($attribute->getCode())) {
unset($attributes[$key]);
}
}
$result = $this->ruleProcessorCopier->copy($sourceProduct, $destinationEntity, $attributes);
if ($result) {
if ($destinationEntity instanceof ProductInterface) {
$this->productSaver->save($destinationEntity);
} else {
$this->productModelSaver->save($destinationEntity);
}
}
}
private function createNewSubProduct(
ProductInterface $sourceProduct,
ProductModelInterface $productModel,
array $attributes
): void {
$destinationProduct = $this->variantProductBuilder->createProduct(
Uuid::uuid4()->toString(),
$productModel->getFamily()->getCode()
);
$destinationProduct->setFamilyVariant($productModel->getFamilyVariant());
$destinationProduct->setParent($productModel);
$this->copy($sourceProduct, $destinationProduct, $attributes);
}
private function createNewSubProductModel(
ProductInterface $sourceProduct,
ProductModelInterface $productModel,
array $attributes
): ProductModelInterface {
$subProductModel = new ProductModel();
$subProductModel->setCreated(new \DateTime());
$subProductModel->setUpdated(new \DateTime());
$subProductModel->setCode(Uuid::uuid4()->toString());
$subProductModel->setFamilyVariant($productModel->getFamilyVariant());
$subProductModel->setParent($productModel);
$this->copy($sourceProduct, $subProductModel, $attributes);
return $subProductModel;
}
}
<?php
/*
* Copyright (c) 2021, VillageReach
* Licensed under the Non-Profit Open Software License version 3.0.
* SPDX-License-Identifier: NPOSL-3.0
*/
declare(strict_types=1);
/*
* Copyright (c) 2021, VillageReach
* Licensed under the Non-Profit Open Software License version 3.0.
* SPDX-License-Identifier: NPOSL-3.0
*/
namespace PcmtRulesBundle\Service\CopyProductsRule;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductInterface;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductModelInterface;
use Akeneo\Pim\Structure\Component\Model\FamilyInterface;
use Akeneo\Tool\Component\Batch\Model\StepExecution;
class CopyProductsRuleProcessor
{
/** @var CopyProductToProductModel */
private $copyProductToProductModel;
public function __construct(CopyProductToProductModel $copyProductToProductModel)
{
$this->copyProductToProductModel = $copyProductToProductModel;
}
public function process(
StepExecution $stepExecution,
FamilyInterface $destinationFamily,
ProductInterface $sourceProduct
): void {
$stepExecution->incrementSummaryInfo('processed', 1);
$associations = $sourceProduct->getAssociations();
foreach ($associations as $association) {
$models = $association->getProductModels();
foreach ($models as $model) {
/** @var ProductModelInterface $model */
$stepExecution->incrementSummaryInfo('associated_product_models_found', 1);
if ($model->getFamily()->getCode() === $destinationFamily->getCode()) {
$stepExecution->incrementSummaryInfo('associated_product_models_found_in_correct_family', 1);
$this->copyProductToProductModel->process($stepExecution, $sourceProduct, $model);
}
}
}
}
}
<?php
/*
* Copyright (c) 2021, VillageReach
* Licensed under the Non-Profit Open Software License version 3.0.
* SPDX-License-Identifier: NPOSL-3.0
*/
declare(strict_types=1);
/*
* Copyright (c) 2021, VillageReach
* Licensed under the Non-Profit Open Software License version 3.0.
* SPDX-License-Identifier: NPOSL-3.0
*/
namespace PcmtRulesBundle\Service\CopyProductsRule;
use Akeneo\Pim\Enrichment\Component\Product\Model\EntityWithValuesInterface;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductInterface;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductModelInterface;
use Doctrine\Common\Collections\Collection;
class SubEntityFinder
{
public function findByAxisAttributes(
ProductModelInterface $productModel,
Collection $axisAttributes,
ProductInterface $sourceProduct
): ?EntityWithValuesInterface {
foreach ($productModel->getProductModels() as $subProductModel) {
if ($this->compareIfEqual($subProductModel, $sourceProduct, $axisAttributes)) {
return $subProductModel;
}
}
foreach ($productModel->getProducts() as $subProduct) {
if ($this->compareIfEqual($subProduct, $sourceProduct, $axisAttributes)) {
return $subProduct;
}
}
return null;
}
private function compareIfEqual(EntityWithValuesInterface $product1, EntityWithValuesInterface $product2, Collection $attributes): bool
{
foreach ($attributes as $attribute) {
$value1 = $product1->getValue($attribute->getCode());
$value2 = $product2->getValue($attribute->getCode());
if (!$value1 || !$value2 || !$value1->isEqual($value2)) {
return false;
}
}
return true;
}
}
\ No newline at end of file
......@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace PcmtRulesBundle\Tests\Connector\Job\Step;
use Akeneo\Pim\Structure\Bundle\Doctrine\ORM\Repository\FamilyRepository;
use Akeneo\Tool\Component\Batch\Job\JobParameters;
use Akeneo\Tool\Component\Batch\Job\JobRepositoryInterface;
use Akeneo\Tool\Component\Batch\Model\StepExecution;
......@@ -32,6 +33,9 @@ class CopyProductsRuleStepTest extends TestCase
/** @var JobParameters|MockObject */
private $jobParametersMock;
/** @var FamilyRepository|MockObject */
private $familyRepositoryMock;
protected function setUp(): void
{
$this->eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
......@@ -40,6 +44,7 @@ class CopyProductsRuleStepTest extends TestCase
$this->jobParametersMock = $this->createMock(JobParameters::class);
$this->jobParametersMock->method('all')->willReturn(['sss' => 'sdsfsd']);
$this->stepExecutionMock->method('getJobParameters')->willReturn($this->jobParametersMock);
$this->familyRepositoryMock = $this->createMock(FamilyRepository::class);
}
public function testDoExecute(): void
......@@ -57,10 +62,13 @@ class CopyProductsRuleStepTest extends TestCase
private function getRuleStepInstance(): CopyProductsRuleStep
{
return new CopyProductsRuleStep(
$step = new CopyProductsRuleStep(
'name',
$this->eventDispatcherMock,
$this->jobRepositoryMock
);
$step->setFamilyRepository($this->familyRepositoryMock);
return $step;
}
}
\ No newline at end of file