custom/plugins/PickwareErpStarter/src/Stocktaking/ProductSummary/StocktakeProductSummaryUpdater.php line 150

Open in your IDE?
  1. <?php
  2. /*
  3.  * Copyright (c) Pickware GmbH. All rights reserved.
  4.  * This file is part of software that is released under a proprietary license.
  5.  * You must not copy, modify, distribute, make publicly available, or execute
  6.  * its contents or parts thereof without express permission by the copyright
  7.  * holder, unless otherwise permitted by law.
  8.  */
  9. declare(strict_types=1);
  10. namespace Pickware\PickwareErpStarter\Stocktaking\ProductSummary;
  11. use Doctrine\DBAL\Connection;
  12. use Pickware\DalBundle\EntityPreWriteValidationEvent;
  13. use Pickware\DalBundle\EntityPreWriteValidationEventDispatcher;
  14. use Pickware\PickwareErpStarter\Stock\WarehouseStockUpdatedEvent;
  15. use Pickware\PickwareErpStarter\Stocktaking\Model\StocktakeCountingProcessDefinition;
  16. use Pickware\PickwareErpStarter\Stocktaking\Model\StocktakeCountingProcessItemDefinition;
  17. use Pickware\PickwareErpStarter\Stocktaking\Model\StocktakeDefinition;
  18. use Shopware\Core\Defaults;
  19. use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
  24. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  25. class StocktakeProductSummaryUpdater implements EventSubscriberInterface
  26. {
  27.     private Connection $connection;
  28.     private StocktakeProductSummaryCalculator $summaryCalculator;
  29.     public function __construct(Connection $dbStocktakeProductSummaryCalculator $summaryCalculator)
  30.     {
  31.         $this->connection $db;
  32.         $this->summaryCalculator $summaryCalculator;
  33.     }
  34.     public static function getSubscribedEvents(): array
  35.     {
  36.         return [
  37.             // Triggered from pickware erp when the warehouse stock of certain products in certain warehouses is updated
  38.             WarehouseStockUpdatedEvent::EVENT_NAME => 'warehouseStockUpdated',
  39.             // Triggered when a counting process item is created or updated
  40.             StocktakeCountingProcessItemDefinition::ENTITY_WRITTEN_EVENT => 'stocktakeCountingProcessItemWritten',
  41.             // Triggered when a counting process item is deleted
  42.             StocktakeCountingProcessItemDefinition::ENTITY_DELETED_EVENT => 'stocktakeCountingProcessItemDeleted',
  43.             // Triggered when a counting process is created or updated (i.e. moved to another stocktake)
  44.             StocktakeCountingProcessDefinition::ENTITY_WRITTEN_EVENT => 'stocktakeCountingProcessWritten',
  45.             // Triggered when a counting process is deleted. The countingProcess.deleted event is also triggered, but
  46.             // as we need additional information to recalculate the correct summaries we need to listen to this event.
  47.             EntityWrittenContainerEvent::class => 'entityWritten',
  48.             // Triggered when a counting process is created or updated
  49.             StocktakeDefinition::ENTITY_WRITTEN_EVENT => 'stocktakeWritten',
  50.             EntityPreWriteValidationEventDispatcher::getEventName(StocktakeCountingProcessItemDefinition::ENTITY_NAME) => 'requestChangeSet',
  51.             EntityPreWriteValidationEventDispatcher::getEventName(StocktakeCountingProcessDefinition::ENTITY_NAME) => 'requestChangeSet',
  52.         ];
  53.     }
  54.     public function requestChangeSet($event): void
  55.     {
  56.         if (!($event instanceof EntityPreWriteValidationEvent)) {
  57.             // The subscriber is probably instantiated in its old version (with the Shopware PreWriteValidationEvent) in
  58.             // the container and will be updated on the next container rebuild (next request). Early return.
  59.             return;
  60.         }
  61.         foreach ($event->getCommands() as $command) {
  62.             if ($command instanceof ChangeSetAware) {
  63.                 $command->requestChangeSet();
  64.             }
  65.         }
  66.     }
  67.     public function warehouseStockUpdated(WarehouseStockUpdatedEvent $event): void
  68.     {
  69.         $productStocktakeCombinations $this->connection->fetchAllAssociative(
  70.             'SELECT
  71.                 HEX(countingProcessItem.`product_id`) as productId,
  72.                 HEX(stocktake.`id`) as stocktakeId
  73.             FROM pickware_erp_stocktaking_stocktake stocktake
  74.                 LEFT JOIN pickware_erp_stocktaking_stocktake_counting_process countingProcess
  75.                     ON stocktake.`id` = countingProcess.`stocktake_id`
  76.                 LEFT JOIN pickware_erp_stocktaking_stocktake_counting_process_item countingProcessItem
  77.                     ON countingProcess.`id` = countingProcessItem.`counting_process_id`
  78.             WHERE
  79.                 stocktake.`warehouse_id` IN (:warehouseIds)
  80.                 AND stocktake.`is_active` = 1
  81.                 AND countingProcessItem.`product_id` IN (:productIds)',
  82.             [
  83.                 'warehouseIds' => array_map('hex2bin'$event->getWarehouseIds()),
  84.                 'productIds' => array_map('hex2bin'$event->getProductIds()),
  85.             ],
  86.             [
  87.                 'warehouseIds' => Connection::PARAM_STR_ARRAY,
  88.                 'productIds' => Connection::PARAM_STR_ARRAY,
  89.             ],
  90.         );
  91.         $this->summaryCalculator->recalculateStocktakeProductSummaries(
  92.             array_unique(array_column($productStocktakeCombinations'productId')),
  93.             array_unique(array_column($productStocktakeCombinations'stocktakeId')),
  94.         );
  95.     }
  96.     public function stocktakeCountingProcessItemWritten(EntityWrittenEvent $event): void
  97.     {
  98.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  99.             return;
  100.         }
  101.         $countingProcessItemIds = [];
  102.         foreach ($event->getWriteResults() as $writeResult) {
  103.             // An array primary key should not be possible (for this entity), but we assert it either way to ensure the
  104.             // handling afterwards does not fault.
  105.             if (!is_string($writeResult->getPrimaryKey())) {
  106.                 continue;
  107.             }
  108.             $countingProcessItemIds[] = $writeResult->getPrimaryKey();
  109.         }
  110.         if (count($countingProcessItemIds) === 0) {
  111.             return;
  112.         }
  113.         $productStocktakeCombinations $this->connection->fetchAllAssociative(
  114.             'SELECT
  115.                 HEX(countingProcessItem.`product_id`) as productId,
  116.                 HEX(countingProcess.`stocktake_id`) as stocktakeId
  117.             FROM `pickware_erp_stocktaking_stocktake_counting_process_item` countingProcessItem
  118.                 LEFT JOIN `pickware_erp_stocktaking_stocktake_counting_process` countingProcess
  119.                     ON countingProcessItem.`counting_process_id` = countingProcess.`id`
  120.                 LEFT JOIN `pickware_erp_stocktaking_stocktake` stocktake
  121.                     ON countingProcess.`stocktake_id` = stocktake.`id`
  122.             WHERE
  123.                   countingProcessItem.`id` IN (:countingProcessItemIds)
  124.                   AND stocktake.`is_active` = 1',
  125.             ['countingProcessItemIds' => array_map('hex2bin'$countingProcessItemIds)],
  126.             ['countingProcessItemIds' => Connection::PARAM_STR_ARRAY],
  127.         );
  128.         $this->summaryCalculator->recalculateStocktakeProductSummaries(
  129.             array_unique(array_column($productStocktakeCombinations'productId')),
  130.             array_unique(array_column($productStocktakeCombinations'stocktakeId')),
  131.         );
  132.     }
  133.     public function stocktakeCountingProcessItemDeleted(EntityDeletedEvent $event): void
  134.     {
  135.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  136.             return;
  137.         }
  138.         $deletedCountingProcessItemIds = [];
  139.         $productIds = [];
  140.         $countingProcessIds = [];
  141.         foreach ($event->getWriteResults() as $writeResult) {
  142.             // An array primary key should not be possible (for this entity), but we assert it either way to ensure the
  143.             // handling afterwards does not fault.
  144.             if ($writeResult->getOperation() !== EntityWriteResult::OPERATION_DELETE
  145.                 || !is_string($writeResult->getPrimaryKey())) {
  146.                 continue;
  147.             }
  148.             $productIds[] = bin2hex($writeResult->getChangeSet()->getBefore('product_id'));
  149.             $countingProcessIds[] = bin2hex($writeResult->getChangeSet()->getBefore('counting_process_id'));
  150.             $deletedCountingProcessItemIds[] = $writeResult->getPrimaryKey();
  151.         }
  152.         if (count($deletedCountingProcessItemIds) === 0) {
  153.             return;
  154.         }
  155.         $stocktakeIds $this->connection->fetchAllAssociative(
  156.             'SELECT HEX(countingProcess.`stocktake_id`) as stocktakeId
  157.             FROM `pickware_erp_stocktaking_stocktake_counting_process` countingProcess
  158.                 LEFT JOIN `pickware_erp_stocktaking_stocktake` stocktake
  159.                     ON countingProcess.`stocktake_id` = stocktake.`id`
  160.             WHERE
  161.                   countingProcess.`id` IN (:countingProcessIds)
  162.                   AND stocktake.`is_active` = 1',
  163.             ['countingProcessIds' => array_map('hex2bin'$countingProcessIds)],
  164.             ['countingProcessIds' => Connection::PARAM_STR_ARRAY],
  165.         );
  166.         // When a counting process item is deleted, recalculate all referenced product summaries without the counting
  167.         // process items that are about to be deleted. This recalculation has to be done before the counting process
  168.         // item is actually deleted, so we can determine the referenced products. This is why the deny-list is used.
  169.         $this->summaryCalculator->recalculateStocktakeProductSummaries(
  170.             $productIds,
  171.             array_unique(array_column($stocktakeIds'stocktakeId')),
  172.             $deletedCountingProcessItemIds,
  173.         );
  174.     }
  175.     public function stocktakeCountingProcessWritten(EntityWrittenEvent $event): void
  176.     {
  177.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  178.             return;
  179.         }
  180.         $countingProcessIds = [];
  181.         $additionalStocktakeIds = [];
  182.         foreach ($event->getWriteResults() as $writeResult) {
  183.             // An array primary key should not be possible (for this entity), but we assert it either way to ensure the
  184.             // handling afterwards does not fault. Additionally, we do not handle DELETE operations here as they are
  185.             // handled in the ::entityWritten method.
  186.             if (!is_string($writeResult->getPrimaryKey())
  187.                 || $writeResult->getOperation() === EntityWriteResult::OPERATION_DELETE) {
  188.                 continue;
  189.             }
  190.             $countingProcessIds[] = $writeResult->getPrimaryKey();
  191.             $changeSet $writeResult->getChangeSet();
  192.             if ($changeSet && $changeSet->hasChanged('stocktake_id')) {
  193.                 // Recalculate the summaries for the old stocktake as it is now possibly missing some counted stock
  194.                 // (e.g. from the items of this counting process)
  195.                 $additionalStocktakeIds[] = bin2hex($changeSet->getBefore('stocktake_id'));
  196.             }
  197.         }
  198.         if (count($countingProcessIds) === 0) {
  199.             return;
  200.         }
  201.         $productStocktakeCombinations $this->connection->fetchAllAssociative(
  202.             'SELECT
  203.                 HEX(countingProcessItem.`product_id`) as productId,
  204.                 HEX(countingProcess.`stocktake_id`) as stocktakeId
  205.             FROM `pickware_erp_stocktaking_stocktake_counting_process_item` countingProcessItem
  206.                 LEFT JOIN `pickware_erp_stocktaking_stocktake_counting_process` countingProcess
  207.                     ON countingProcessItem.`counting_process_id` = countingProcess.`id`
  208.                 LEFT JOIN `pickware_erp_stocktaking_stocktake` stocktake
  209.                     ON countingProcess.`stocktake_id` = stocktake.`id`
  210.             WHERE
  211.                   countingProcess.`id` IN (:countingProcessIds)
  212.                   AND stocktake.`is_active` = 1',
  213.             ['countingProcessIds' => array_map('hex2bin'$countingProcessIds)],
  214.             ['countingProcessIds' => Connection::PARAM_STR_ARRAY],
  215.         );
  216.         // Recalculate the summaries for the given products and stocktakes, but do not generate
  217.         $this->summaryCalculator->recalculateStocktakeProductSummaries(
  218.             array_unique(array_column($productStocktakeCombinations'productId')),
  219.             array_unique(array_merge(
  220.                 array_column($productStocktakeCombinations'stocktakeId'),
  221.                 $additionalStocktakeIds,
  222.             )),
  223.         );
  224.     }
  225.     public function entityWritten(EntityWrittenContainerEvent $event): void
  226.     {
  227.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  228.             return;
  229.         }
  230.         $deletedCountingProcessIds $event->getDeletedPrimaryKeys(StocktakeCountingProcessDefinition::ENTITY_NAME);
  231.         if (count($deletedCountingProcessIds) === 0) {
  232.             return;
  233.         }
  234.         // As this event is dispatched only after the counting processes themselves were deleted which through foreign
  235.         // keys also entails the counting process items, we cannot access the affected products and stocktakes with
  236.         // their primary keys only. We thus have to fetch the affected product and stocktake ids from the item event
  237.         // in the given container event.
  238.         $countingProcessItemEvent $event->getEventByEntityName(StocktakeCountingProcessItemDefinition::ENTITY_NAME);
  239.         // Incase no counting process items were deleted with the counting process (e.g. it had none attached), we can
  240.         // return early.
  241.         if (!$countingProcessItemEvent) {
  242.             return;
  243.         }
  244.         $productIds = [];
  245.         $deletedCountingProcessItemIds = [];
  246.         foreach ($countingProcessItemEvent->getWriteResults() as $writeResult) {
  247.             // An array primary key should not be possible (for this entity), but we assert it either way to ensure the
  248.             // handling afterwards does not fault.
  249.             if ($writeResult->getOperation() !== EntityWriteResult::OPERATION_DELETE
  250.                 || !is_string($writeResult->getPrimaryKey())) {
  251.                 continue;
  252.             }
  253.             // Only consider products of items that were deleted with a counting process. Single item deletion is
  254.             // handled in ::stocktakeCountingProcessItemWritten()
  255.             if (!in_array(bin2hex($writeResult->getChangeSet()->getBefore('counting_process_id')), $deletedCountingProcessIds)) {
  256.                 continue;
  257.             }
  258.             $productIds[] = bin2hex($writeResult->getChangeSet()->getBefore('product_id'));
  259.             $deletedCountingProcessItemIds[] = $writeResult->getPrimaryKey();
  260.         }
  261.         $stocktakeCountingProcessEvent $event->getEventByEntityName(StocktakeCountingProcessDefinition::ENTITY_NAME);
  262.         $stocktakeIds = [];
  263.         foreach ($stocktakeCountingProcessEvent->getWriteResults() as $writeResult) {
  264.             if ($writeResult->getOperation() !== EntityWriteResult::OPERATION_DELETE) {
  265.                 continue;
  266.             }
  267.             $stocktakeIds[] = bin2hex($writeResult->getChangeSet()->getBefore('stocktake_id'));
  268.         }
  269.         $this->summaryCalculator->recalculateStocktakeProductSummaries(
  270.             $productIds,
  271.             $stocktakeIds,
  272.             $deletedCountingProcessItemIds,
  273.         );
  274.     }
  275.     public function stocktakeWritten(EntityWrittenEvent $event): void
  276.     {
  277.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  278.             return;
  279.         }
  280.         $stocktakeIds = [];
  281.         foreach ($event->getWriteResults() as $writeResult) {
  282.             // An array primary key should not be possible (for this entity), but we assert it either way to ensure the
  283.             // handling afterwards does not fault.
  284.             if (!is_string($writeResult->getPrimaryKey())) {
  285.                 continue;
  286.             }
  287.             $changeSet $writeResult->getChangeSet();
  288.             // This entity written event is only relevant for when the stocktake was changed from inactive to active
  289.             // (= setting the related importExportId from not-null to null). We need to recalculate all product
  290.             // summaries for that stocktake in this case, as recalculation is skipped for inactive stocktakes and the
  291.             // summaries may be out of date.
  292.             if ($changeSet && (!$changeSet->hasChanged('import_export_id') || $changeSet->getAfter('import_export_id') !== null)) {
  293.                 continue;
  294.             }
  295.             $stocktakeIds[] = $writeResult->getPrimaryKey();
  296.         }
  297.         if (count($stocktakeIds) === 0) {
  298.             return;
  299.         }
  300.         // We know that each stocktake (id) that made it thus far must be active, so this assertion is skipped in the
  301.         // SQL statement
  302.         $productStocktakeCombinations $this->connection->fetchAllAssociative(
  303.             'SELECT
  304.                 HEX(countingProcessItem.`product_id`) as productId,
  305.                 HEX(countingProcess.`stocktake_id`) as stocktakeId
  306.             FROM `pickware_erp_stocktaking_stocktake_counting_process_item` countingProcessItem
  307.                 LEFT JOIN `pickware_erp_stocktaking_stocktake_counting_process` countingProcess
  308.                     ON countingProcessItem.`counting_process_id` = countingProcess.`id`
  309.             WHERE countingProcess.`stocktake_id` IN (:stocktakeIds)',
  310.             ['stocktakeIds' => array_map('hex2bin'$stocktakeIds)],
  311.             ['stocktakeIds' => Connection::PARAM_STR_ARRAY],
  312.         );
  313.         $this->summaryCalculator->recalculateStocktakeProductSummaries(
  314.             array_unique(array_column($productStocktakeCombinations'productId')),
  315.             array_unique(array_column($productStocktakeCombinations'stocktakeId')),
  316.         );
  317.     }
  318. }