core/lib/Thelia/Action/ProductSaleElement.php line 272

  1. <?php
  2. /*
  3.  * This file is part of the Thelia package.
  4.  * http://www.thelia.net
  5.  *
  6.  * (c) OpenStudio <info@thelia.net>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Thelia\Action;
  12. use Propel\Runtime\ActiveQuery\Criteria;
  13. use Propel\Runtime\Connection\ConnectionInterface;
  14. use Propel\Runtime\Propel;
  15. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  16. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  17. use Thelia\Core\Event\Product\ProductCloneEvent;
  18. use Thelia\Core\Event\Product\ProductCombinationGenerationEvent;
  19. use Thelia\Core\Event\ProductSaleElement\ProductSaleElementCreateEvent;
  20. use Thelia\Core\Event\ProductSaleElement\ProductSaleElementDeleteEvent;
  21. use Thelia\Core\Event\ProductSaleElement\ProductSaleElementUpdateEvent;
  22. use Thelia\Core\Event\TheliaEvents;
  23. use Thelia\Core\Template\Loop\ProductSaleElementsDocument;
  24. use Thelia\Core\Template\Loop\ProductSaleElementsImage;
  25. use Thelia\Core\Translation\Translator;
  26. use Thelia\Model\AttributeAvQuery;
  27. use Thelia\Model\AttributeCombination;
  28. use Thelia\Model\AttributeCombinationQuery;
  29. use Thelia\Model\Map\AttributeCombinationTableMap;
  30. use Thelia\Model\Map\ProductSaleElementsTableMap;
  31. use Thelia\Model\ProductDocumentQuery;
  32. use Thelia\Model\ProductImageQuery;
  33. use Thelia\Model\ProductPrice;
  34. use Thelia\Model\ProductPriceQuery;
  35. use Thelia\Model\ProductSaleElements;
  36. use Thelia\Model\ProductSaleElementsProductDocument;
  37. use Thelia\Model\ProductSaleElementsProductDocumentQuery;
  38. use Thelia\Model\ProductSaleElementsProductImage;
  39. use Thelia\Model\ProductSaleElementsProductImageQuery;
  40. use Thelia\Model\ProductSaleElementsQuery;
  41. class ProductSaleElement extends BaseAction implements EventSubscriberInterface
  42. {
  43.     /** @var EventDispatcherInterface */
  44.     protected $eventDispatcher;
  45.     public function __construct(EventDispatcherInterface $eventDispatcher)
  46.     {
  47.         $this->eventDispatcher $eventDispatcher;
  48.     }
  49.     /**
  50.      * Create a new product sale element, with or without combination.
  51.      *
  52.      * @throws \Exception
  53.      */
  54.     public function create(ProductSaleElementCreateEvent $event): void
  55.     {
  56.         $con Propel::getWriteConnection(ProductSaleElementsTableMap::DATABASE_NAME);
  57.         $con->beginTransaction();
  58.         try {
  59.             // Check if we have a PSE without combination, this is the "default" PSE. Attach the combination to this PSE
  60.             $salesElement ProductSaleElementsQuery::create()
  61.                 ->filterByProductId($event->getProduct()->getId())
  62.                 ->joinAttributeCombination(nullCriteria::LEFT_JOIN)
  63.                 ->add(AttributeCombinationTableMap::COL_PRODUCT_SALE_ELEMENTS_IDnullCriteria::ISNULL)
  64.                 ->findOne($con);
  65.             if ($salesElement == null) {
  66.                 // Create a new default product sale element
  67.                 $salesElement $event->getProduct()->createProductSaleElement($con000$event->getCurrencyId(), false);
  68.             } else {
  69.                 // This (new) one is the default
  70.                 $salesElement->setIsDefault(true)->save($con);
  71.             }
  72.             // Attach combination, if defined.
  73.             $combinationAttributes $event->getAttributeAvList();
  74.             if (\count($combinationAttributes) > 0) {
  75.                 foreach ($combinationAttributes as $attributeAvId) {
  76.                     $attributeAv AttributeAvQuery::create()->findPk($attributeAvId);
  77.                     if ($attributeAv !== null) {
  78.                         $attributeCombination = new AttributeCombination();
  79.                         $attributeCombination
  80.                             ->setAttributeAvId($attributeAvId)
  81.                             ->setAttribute($attributeAv->getAttribute())
  82.                             ->setProductSaleElements($salesElement)
  83.                             ->save($con);
  84.                     }
  85.                 }
  86.             }
  87.             $event->setProductSaleElement($salesElement);
  88.             // Store all the stuff !
  89.             $con->commit();
  90.         } catch (\Exception $ex) {
  91.             $con->rollback();
  92.             throw $ex;
  93.         }
  94.     }
  95.     /**
  96.      * Update an existing product sale element.
  97.      *
  98.      * @throws \Exception
  99.      */
  100.     public function update(ProductSaleElementUpdateEvent $event): void
  101.     {
  102.         $salesElement ProductSaleElementsQuery::create()->findPk($event->getProductSaleElementId());
  103.         $con Propel::getWriteConnection(ProductSaleElementsTableMap::DATABASE_NAME);
  104.         $con->beginTransaction();
  105.         try {
  106.             // Update the product's tax rule
  107.             $event->getProduct()->setTaxRuleId($event->getTaxRuleId())->save($con);
  108.             // If product sale element is not defined, create it.
  109.             if ($salesElement == null) {
  110.                 $salesElement = new ProductSaleElements();
  111.                 $salesElement->setProduct($event->getProduct());
  112.             }
  113.             $defaultStatus $event->getIsDefault();
  114.             // If this PSE will become the default one, be sure to have *only one* default for this product
  115.             if ($defaultStatus) {
  116.                 ProductSaleElementsQuery::create()
  117.                     ->filterByProduct($event->getProduct())
  118.                     ->filterByIsDefault(true)
  119.                     ->filterById($event->getProductSaleElementId(), Criteria::NOT_EQUAL)
  120.                     ->update(['IsDefault' => false], $con)
  121.                 ;
  122.             } else {
  123.                 // We will not allow the default PSE to become non default if no other default PSE exists for this product.
  124.                 if ($salesElement->getIsDefault() && ProductSaleElementsQuery::create()
  125.                         ->filterByProduct($event->getProduct())
  126.                         ->filterByIsDefault(true)
  127.                         ->filterById($salesElement->getId(), Criteria::NOT_EQUAL)
  128.                         ->count() === 0) {
  129.                     // Prevent setting the only default PSE to non-default
  130.                     $defaultStatus true;
  131.                 }
  132.             }
  133.             // Update sale element
  134.             $salesElement
  135.                 ->setRef($event->getReference())
  136.                 ->setQuantity($event->getQuantity())
  137.                 ->setPromo($event->getOnsale())
  138.                 ->setNewness($event->getIsnew())
  139.                 ->setWeight($event->getWeight())
  140.                 ->setIsDefault($defaultStatus)
  141.                 ->setEanCode($event->getEanCode())
  142.                 ->save()
  143.             ;
  144.             // Update/create price for current currency
  145.             $productPrice ProductPriceQuery::create()
  146.                 ->filterByCurrencyId($event->getCurrencyId())
  147.                 ->filterByProductSaleElementsId($salesElement->getId())
  148.                 ->findOne($con);
  149.             // If price is not defined, create it.
  150.             if ($productPrice == null) {
  151.                 $productPrice = new ProductPrice();
  152.                 $productPrice
  153.                     ->setProductSaleElements($salesElement)
  154.                     ->setCurrencyId($event->getCurrencyId())
  155.                 ;
  156.             }
  157.             // Check if we have to store the price
  158.             $productPrice->setFromDefaultCurrency($event->getFromDefaultCurrency());
  159.             if ($event->getFromDefaultCurrency() == 0) {
  160.                 // Store the price
  161.                 $productPrice
  162.                     ->setPromoPrice($event->getSalePrice())
  163.                     ->setPrice($event->getPrice())
  164.                 ;
  165.             } else {
  166.                 // Do not store the price.
  167.                 $productPrice
  168.                     ->setPromoPrice(0)
  169.                     ->setPrice(0)
  170.                 ;
  171.             }
  172.             $productPrice->save($con);
  173.             // Store all the stuff !
  174.             $con->commit();
  175.         } catch (\Exception $ex) {
  176.             $con->rollback();
  177.             throw $ex;
  178.         }
  179.     }
  180.     /**
  181.      * Delete a product sale element.
  182.      *
  183.      * @throws \Exception
  184.      */
  185.     public function delete(ProductSaleElementDeleteEvent $event): void
  186.     {
  187.         if (null !== $pse ProductSaleElementsQuery::create()->findPk($event->getProductSaleElementId())) {
  188.             $product $pse->getProduct();
  189.             $con Propel::getWriteConnection(ProductSaleElementsTableMap::DATABASE_NAME);
  190.             $con->beginTransaction();
  191.             try {
  192.                 // If we are deleting the last PSE of the product, don(t delete it, but insteaf
  193.                 // transform it into a PSE detached for any attribute combination, so that product
  194.                 // prices, weight, stock and attributes will not be lost.
  195.                 if ($product->countSaleElements($con) === 1) {
  196.                     $pse
  197.                         ->setIsDefault(true)
  198.                         ->save($con);
  199.                     // Delete the related attribute combination.
  200.                     AttributeCombinationQuery::create()
  201.                         ->filterByProductSaleElementsId($pse->getId())
  202.                         ->delete($con);
  203.                 } else {
  204.                     // Delete the PSE
  205.                     $pse->delete($con);
  206.                     // If we deleted the default PSE, make the last created one the default
  207.                     if ($pse->getIsDefault()) {
  208.                         $newDefaultPse ProductSaleElementsQuery::create()
  209.                             ->filterByProductId($product->getId())
  210.                             ->filterById($pse->getId(), Criteria::NOT_EQUAL)
  211.                             ->orderByCreatedAt(Criteria::DESC)
  212.                             ->findOne($con)
  213.                         ;
  214.                         if (null !== $newDefaultPse) {
  215.                             $newDefaultPse->setIsDefault(true)->save($con);
  216.                         }
  217.                     }
  218.                 }
  219.                 // Store all the stuff !
  220.                 $con->commit();
  221.             } catch (\Exception $ex) {
  222.                 $con->rollback();
  223.                 throw $ex;
  224.             }
  225.         }
  226.     }
  227.     /**
  228.      * Generate combinations. All existing combinations for the product are deleted.
  229.      *
  230.      * @throws \Exception
  231.      */
  232.     public function generateCombinations(ProductCombinationGenerationEvent $event): void
  233.     {
  234.         $con Propel::getWriteConnection(ProductSaleElementsTableMap::DATABASE_NAME);
  235.         $con->beginTransaction();
  236.         try {
  237.             // Delete all product's productSaleElement
  238.             ProductSaleElementsQuery::create()->filterByProductId($event->product->getId())->delete();
  239.             $isDefault true;
  240.             // Create all combinations
  241.             foreach ($event->getCombinations() as $combinationAttributesAvIds) {
  242.                 // Create the PSE
  243.                 $saleElement $event->getProduct()->createProductSaleElement(
  244.                     $con,
  245.                     $event->getWeight(),
  246.                     $event->getPrice(),
  247.                     $event->getSalePrice(),
  248.                     $event->getCurrencyId(),
  249.                     $isDefault,
  250.                     $event->getOnsale(),
  251.                     $event->getIsnew(),
  252.                     $event->getQuantity(),
  253.                     $event->getEanCode(),
  254.                     $event->getReference()
  255.                 );
  256.                 $isDefault false;
  257.                 $this->createCombination($con$saleElement$combinationAttributesAvIds);
  258.             }
  259.             // Store all the stuff !
  260.             $con->commit();
  261.         } catch (\Exception $ex) {
  262.             $con->rollback();
  263.             throw $ex;
  264.         }
  265.     }
  266.     /**
  267.      * Create a combination for a given product sale element.
  268.      *
  269.      * @param ConnectionInterface $con                   the Propel connection
  270.      * @param ProductSaleElements $salesElement          the product sale element
  271.      * @param array               $combinationAttributes an array oif attributes av IDs
  272.      *
  273.      * @throws \Propel\Runtime\Exception\PropelException
  274.      */
  275.     protected function createCombination(ConnectionInterface $conProductSaleElements $salesElement$combinationAttributes): void
  276.     {
  277.         foreach ($combinationAttributes as $attributeAvId) {
  278.             $attributeAv AttributeAvQuery::create()->findPk($attributeAvId);
  279.             if ($attributeAv !== null) {
  280.                 $attributeCombination = new AttributeCombination();
  281.                 $attributeCombination
  282.                     ->setAttributeAvId($attributeAvId)
  283.                     ->setAttribute($attributeAv->getAttribute())
  284.                     ->setProductSaleElements($salesElement)
  285.                     ->save($con);
  286.             }
  287.         }
  288.     }
  289.     /*******************
  290.      * CLONING PROCESS *
  291.      *******************/
  292.     /**
  293.      * Clone product's PSEs and associated datas.
  294.      *
  295.      * @throws \Propel\Runtime\Exception\PropelException
  296.      */
  297.     public function clonePSE(ProductCloneEvent $event): void
  298.     {
  299.         $clonedProduct $event->getClonedProduct();
  300.         // Get original product's PSEs
  301.         $originalProductPSEs ProductSaleElementsQuery::create()
  302.             ->orderByIsDefault(Criteria::DESC)
  303.             ->findByProductId($event->getOriginalProduct()->getId());
  304.         /**
  305.          * Handle PSEs.
  306.          *
  307.          * @var int                 $key
  308.          * @var ProductSaleElements $originalProductPSE
  309.          */
  310.         foreach ($originalProductPSEs as $key => $originalProductPSE) {
  311.             $currencyId ProductPriceQuery::create()
  312.                 ->filterByProductSaleElementsId($originalProductPSE->getId())
  313.                 ->select('CURRENCY_ID')
  314.                 ->findOne();
  315.             // The default PSE, created at the same time as the clone product, is overwritten
  316.             $clonedProductPSEId $this->createClonePSE($event$originalProductPSE$currencyId);
  317.             $this->updateClonePSE($event$clonedProductPSEId$originalProductPSE$key);
  318.             // PSE associated images
  319.             $originalProductPSEImages ProductSaleElementsProductImageQuery::create()
  320.                 ->findByProductSaleElementsId($originalProductPSE->getId());
  321.             if (null !== $originalProductPSEImages) {
  322.                 $this->clonePSEAssociatedFiles($clonedProduct->getId(), $clonedProductPSEId$originalProductPSEImages$type 'image');
  323.             }
  324.             // PSE associated documents
  325.             $originalProductPSEDocuments ProductSaleElementsProductDocumentQuery::create()
  326.                 ->findByProductSaleElementsId($originalProductPSE->getId());
  327.             if (null !== $originalProductPSEDocuments) {
  328.                 $this->clonePSEAssociatedFiles($clonedProduct->getId(), $clonedProductPSEId$originalProductPSEDocuments$type 'document');
  329.             }
  330.         }
  331.     }
  332.     /**
  333.      * @throws \Propel\Runtime\Exception\PropelException
  334.      *
  335.      * @return int
  336.      */
  337.     public function createClonePSE(ProductCloneEvent $eventProductSaleElements $originalProductPSE$currencyId)
  338.     {
  339.         $attributeCombinationList AttributeCombinationQuery::create()
  340.             ->filterByProductSaleElementsId($originalProductPSE->getId())
  341.             ->select(['ATTRIBUTE_AV_ID'])
  342.             ->find();
  343.         $clonedProductCreatePSEEvent = new ProductSaleElementCreateEvent($event->getClonedProduct(), $attributeCombinationList$currencyId);
  344.         $this->eventDispatcher->dispatch($clonedProductCreatePSEEventTheliaEvents::PRODUCT_ADD_PRODUCT_SALE_ELEMENT);
  345.         return $clonedProductCreatePSEEvent->getProductSaleElement()->getId();
  346.     }
  347.     public function updateClonePSE(ProductCloneEvent $event$clonedProductPSEIdProductSaleElements $originalProductPSE$key): void
  348.     {
  349.         $originalProductPSEPrice ProductPriceQuery::create()
  350.             ->findOneByProductSaleElementsId($originalProductPSE->getId());
  351.         $clonedProductUpdatePSEEvent = new ProductSaleElementUpdateEvent($event->getClonedProduct(), $clonedProductPSEId);
  352.         $clonedProductUpdatePSEEvent
  353.             ->setReference($event->getClonedProduct()->getRef().'-'.($key 1))
  354.             ->setIsdefault($originalProductPSE->getIsDefault())
  355.             ->setFromDefaultCurrency(0)
  356.             ->setWeight($originalProductPSE->getWeight())
  357.             ->setQuantity($originalProductPSE->getQuantity())
  358.             ->setOnsale($originalProductPSE->getPromo())
  359.             ->setIsnew($originalProductPSE->getNewness())
  360.             ->setEanCode($originalProductPSE->getEanCode())
  361.             ->setTaxRuleId($event->getOriginalProduct()->getTaxRuleId())
  362.             ->setPrice($originalProductPSEPrice->getPrice())
  363.             ->setSalePrice($originalProductPSEPrice->getPromoPrice())
  364.             ->setCurrencyId($originalProductPSEPrice->getCurrencyId());
  365.         $this->eventDispatcher->dispatch($clonedProductUpdatePSEEventTheliaEvents::PRODUCT_UPDATE_PRODUCT_SALE_ELEMENT);
  366.     }
  367.     /**
  368.      * @throws \Propel\Runtime\Exception\PropelException
  369.      */
  370.     public function clonePSEAssociatedFiles($clonedProductId$clonedProductPSEId$originalProductPSEFiles$type): void
  371.     {
  372.         /** @var ProductSaleElementsDocument|ProductSaleElementsImage $originalProductPSEFile */
  373.         foreach ($originalProductPSEFiles as $originalProductPSEFile) {
  374.             $originalProductFilePositionQuery = [];
  375.             $originalProductPSEFileId null;
  376.             if (!\in_array($type, ['image''document'])) {
  377.                 throw new \Exception(Translator::getInstance()->trans('Cloning files of type %type is not allowed.', ['%type' => $type], 'core'));
  378.             }
  379.             // Get file's original position
  380.             switch ($type) {
  381.                 case 'image':
  382.                     $originalProductFilePositionQuery ProductImageQuery::create();
  383.                     $originalProductPSEFileId $originalProductPSEFile->getProductImageId();
  384.                     break;
  385.                 case 'document':
  386.                     $originalProductFilePositionQuery ProductDocumentQuery::create();
  387.                     $originalProductPSEFileId $originalProductPSEFile->getProductDocumentId();
  388.                     break;
  389.             }
  390.             $originalProductFilePosition $originalProductFilePositionQuery
  391.                 ->select(['POSITION'])
  392.                 ->findPk($originalProductPSEFileId);
  393.             $clonedProductFileIdToLinkToPSEQuery '';
  394.             // Get cloned file ID to link to the cloned PSE
  395.             switch ($type) {
  396.                 case 'image':
  397.                     $clonedProductFileIdToLinkToPSEQuery ProductImageQuery::create();
  398.                     break;
  399.                 case 'document':
  400.                     $clonedProductFileIdToLinkToPSEQuery ProductDocumentQuery::create();
  401.                     break;
  402.             }
  403.             $clonedProductFileIdToLinkToPSE $clonedProductFileIdToLinkToPSEQuery
  404.                 ->filterByProductId($clonedProductId)
  405.                 ->filterByPosition($originalProductFilePosition)
  406.                 ->select(['ID'])
  407.                 ->findOne();
  408.             $assoc null;
  409.             // Save association
  410.             switch ($type) {
  411.                 case 'image':
  412.                     $assoc = new ProductSaleElementsProductImage();
  413.                     $assoc->setProductImageId($clonedProductFileIdToLinkToPSE);
  414.                     break;
  415.                 case 'document':
  416.                     $assoc = new ProductSaleElementsProductDocument();
  417.                     $assoc->setProductDocumentId($clonedProductFileIdToLinkToPSE);
  418.                     break;
  419.             }
  420.             $assoc
  421.                 ->setProductSaleElementsId($clonedProductPSEId)
  422.                 ->save();
  423.         }
  424.     }
  425.     /***************
  426.      * END CLONING *
  427.      ***************/
  428.     /**
  429.      * {@inheritDoc}
  430.      */
  431.     public static function getSubscribedEvents()
  432.     {
  433.         return [
  434.             TheliaEvents::PRODUCT_ADD_PRODUCT_SALE_ELEMENT => ['create'128],
  435.             TheliaEvents::PRODUCT_UPDATE_PRODUCT_SALE_ELEMENT => ['update'128],
  436.             TheliaEvents::PRODUCT_DELETE_PRODUCT_SALE_ELEMENT => ['delete'128],
  437.             TheliaEvents::PRODUCT_COMBINATION_GENERATION => ['generateCombinations'128],
  438.             TheliaEvents::PSE_CLONE => ['clonePSE'128],
  439.         ];
  440.     }
  441. }