custom/plugins/PickwareErpStarter/src/Stock/WarehouseStockUpdater.php line 53

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\Stock;
  11. use Doctrine\DBAL\Connection;
  12. use Pickware\DalBundle\RetryableTransaction;
  13. use Pickware\PickwareErpStarter\Stock\Model\LocationTypeDefinition;
  14. use Pickware\PickwareErpStarter\Stock\Model\StockMovementDefinition;
  15. use Shopware\Core\Defaults;
  16. use Shopware\Core\Framework\Context;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  18. use Shopware\Core\Framework\Uuid\Uuid;
  19. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  20. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  21. class WarehouseStockUpdater implements EventSubscriberInterface
  22. {
  23.     private Connection $db;
  24.     private EventDispatcherInterface $eventDispatcher;
  25.     private WarehouseStockInitializer $warehouseStockInitializer;
  26.     /**
  27.      * @deprecated next major version: $eventDispatcher argument will be non-optional
  28.      */
  29.     public function __construct(
  30.         Connection $db,
  31.         ?EventDispatcherInterface $eventDispatcher,
  32.         ?WarehouseStockInitializer $warehouseStockInitializer
  33.     ) {
  34.         $this->db $db;
  35.         if ($eventDispatcher) {
  36.             $this->eventDispatcher $eventDispatcher;
  37.         }
  38.         if ($warehouseStockInitializer) {
  39.             $this->warehouseStockInitializer $warehouseStockInitializer;
  40.         }
  41.     }
  42.     public static function getSubscribedEvents(): array
  43.     {
  44.         return [StockMovementDefinition::ENTITY_WRITTEN_EVENT => 'stockMovementWritten'];
  45.     }
  46.     public function stockMovementWritten(EntityWrittenEvent $entityWrittenEvent): void
  47.     {
  48.         if ($entityWrittenEvent->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  49.             return;
  50.         }
  51.         $stockMovementIds = [];
  52.         foreach ($entityWrittenEvent->getWriteResults() as $writeResult) {
  53.             if ($writeResult->getExistence()->exists()) {
  54.                 // Updating stock movements is not supported yet
  55.                 // In case a stock location is deleted, this code path is also reached. This is because an
  56.                 // EntityWrittenEvent is triggered when an entity field gets null-ed because of a SET NULL constraint
  57.                 // of a FK.
  58.                 continue;
  59.             }
  60.             $payload $writeResult->getPayload();
  61.             $stockMovementIds[] = $payload['id'];
  62.         }
  63.         $this->indexStockMovements($stockMovementIds$entityWrittenEvent->getContext());
  64.     }
  65.     /**
  66.      * Updates the warehouse stocks incrementally by iterating the given stock movements.
  67.      *
  68.      * DEPENDS ON `pickware_erp_warehouse_stock` being correctly calculated for all other stock movements for the same
  69.      * products and warehouse stocks besides the given ones.
  70.      */
  71.     public function indexStockMovements(array $stockMovementIdsContext $context): void
  72.     {
  73.         $stockMovementIds array_values(array_unique($stockMovementIds));
  74.         $stockMovements $this->db->fetchAllAssociative(
  75.             'SELECT
  76.                 LOWER(HEX(product_id)) AS productId,
  77.                 LOWER(HEX(product_version_id)) AS productVersionId,
  78.                 quantity,
  79.                 LOWER(HEX(COALESCE(
  80.                     source_warehouse_id,
  81.                     sourceBinLocation.warehouse_id
  82.                 ))) AS sourceWarehouseId,
  83.                 LOWER(HEX(COALESCE(
  84.                     destination_warehouse_id,
  85.                     destinationBinLocation.warehouse_id
  86.                 ))) AS destinationWarehouseId
  87.             FROM pickware_erp_stock_movement stockMovement
  88.             LEFT JOIN pickware_erp_bin_location sourceBinLocation ON sourceBinLocation.id = stockMovement.source_bin_location_id
  89.             LEFT JOIN pickware_erp_bin_location destinationBinLocation ON destinationBinLocation.id = stockMovement.destination_bin_location_id
  90.             WHERE stockMovement.id IN (:stockMovementIds) AND product_version_id = :liveVersionId',
  91.             [
  92.                 'stockMovementIds' => array_map('hex2bin'$stockMovementIds),
  93.                 'liveVersionId' => hex2bin(Defaults::LIVE_VERSION),
  94.             ],
  95.             [
  96.                 'stockMovementIds' => Connection::PARAM_STR_ARRAY,
  97.             ],
  98.         );
  99.         // Update warehouse stocks if stock was moved to or from a warehouse or bin location (in that warehouse) or
  100.         // stock container (in that warehouse).
  101.         $warehouseIds = [];
  102.         $productIds = [];
  103.         foreach ($stockMovements as $stockMovement) {
  104.             if ($stockMovement['sourceWarehouseId'] === $stockMovement['destinationWarehouseId']) {
  105.                 // If the source and destination warehouse is identical (e.g. a stock move from one location in the
  106.                 // warehouse to another location in that warehouse), we do not need to track that warehouse id for the
  107.                 // warehouse stock change event. Because the stock in that warehouse did not change (stock ∓0).
  108.                 continue;
  109.             }
  110.             $productIds array_unique(array_merge(
  111.                 $productIds,
  112.                 [$stockMovement['productId']],
  113.             ));
  114.             if ($stockMovement['sourceWarehouseId']) {
  115.                 $this->persistWarehouseStockChange([
  116.                     'productId' => $stockMovement['productId'],
  117.                     'productVersionId' => $stockMovement['productVersionId'],
  118.                     'warehouseId' => $stockMovement['sourceWarehouseId'],
  119.                     'changeAmount' => -$stockMovement['quantity'],
  120.                 ]);
  121.                 $warehouseIds array_unique(array_merge(
  122.                     $warehouseIds,
  123.                     [$stockMovement['sourceWarehouseId']],
  124.                 ));
  125.             }
  126.             if ($stockMovement['destinationWarehouseId']) {
  127.                 $this->persistWarehouseStockChange([
  128.                     'productId' => $stockMovement['productId'],
  129.                     'productVersionId' => $stockMovement['productVersionId'],
  130.                     'warehouseId' => $stockMovement['destinationWarehouseId'],
  131.                     'changeAmount' => $stockMovement['quantity'],
  132.                 ]);
  133.                 $warehouseIds array_unique(array_merge(
  134.                     $warehouseIds,
  135.                     [$stockMovement['destinationWarehouseId']],
  136.                 ));
  137.             }
  138.         }
  139.         if (count($warehouseIds) > 0) {
  140.             $this->eventDispatcher->dispatch(
  141.                 new WarehouseStockUpdatedEvent($warehouseIds$productIds$context),
  142.                 WarehouseStockUpdatedEvent::EVENT_NAME,
  143.             );
  144.         }
  145.     }
  146.     private function persistWarehouseStockChange(array $payload): void
  147.     {
  148.         $this->db->executeStatement(
  149.             'INSERT INTO pickware_erp_warehouse_stock (
  150.                 id,
  151.                 product_id,
  152.                 product_version_id,
  153.                 quantity,
  154.                 warehouse_id,
  155.                 created_at
  156.             ) VALUES (
  157.                 :id,
  158.                 :productId,
  159.                 :productVersionId,
  160.                 :changeAmount,
  161.                 :warehouseId,
  162.                 NOW(3)
  163.             ) ON DUPLICATE KEY UPDATE
  164.                 quantity = quantity + VALUES(quantity),
  165.                 updated_at = NOW(3)',
  166.             [
  167.                 'id' => Uuid::randomBytes(),
  168.                 'productId' => hex2bin($payload['productId']),
  169.                 'productVersionId' => hex2bin($payload['productVersionId']),
  170.                 'warehouseId' => hex2bin($payload['warehouseId']),
  171.                 'changeAmount' => $payload['changeAmount'],
  172.             ],
  173.         );
  174.     }
  175.     /**
  176.      * This is the indexer scenario. Updates all warehouse stocks for the given products.
  177.      *
  178.      * DEPENDS ON pickware_erp_stock to have been calculated before for the given products.
  179.      *
  180.      * The warehouse stocks are summed up from all warehouse-stock-relevant `pickware_erp_stock`s. The reference to the
  181.      * respective warehouse needs to be manually selected (not automatically). Therefore, when the list of
  182.      * warehouse-stock-relevant stock locations changes, we also need to update this query!
  183.      *
  184.      * @param String[] $productIds
  185.      */
  186.     public function calculateWarehouseStockForProducts(array $productIds): void
  187.     {
  188.         RetryableTransaction::retryable($this->db, function () use ($productIds): void {
  189.             $this->warehouseStockInitializer->ensureProductWarehouseStockForProductsExist($productIds);
  190.         });
  191.         RetryableTransaction::retryable($this->db, function () use ($productIds): void {
  192.             $this->db->executeStatement(
  193.                 'UPDATE `pickware_erp_warehouse_stock`
  194.                  LEFT JOIN (
  195.                      SELECT
  196.                         stock.`product_id` as productId,
  197.                         stock.`product_version_id` as productVersionId,
  198.                         SUM(stock.`quantity`) as quantity,
  199.                         COALESCE(
  200.                             stock.`warehouse_id`, # stock location "warehouse"
  201.                             binLocation.`warehouse_id` # stock location "bin_location"
  202.                         ) as warehouseId
  203.                     FROM `pickware_erp_stock` stock
  204.                     LEFT JOIN `pickware_erp_bin_location` binLocation
  205.                     ON stock.`bin_location_id` = binLocation.`id`
  206.                     WHERE stock.`product_id` IN (:productIds)
  207.                     AND stock.`product_version_id` = :liveVersionId
  208.                     AND stock.`location_type_technical_name` IN (:warehouseStockRelevantStockLocationTypes)
  209.                     GROUP BY COALESCE(
  210.                         stock.`warehouse_id`,
  211.                         binLocation.`warehouse_id`
  212.                     ),
  213.                     stock.`product_id`,
  214.                     stock.`product_version_id`
  215.                 ) newWarehouseStocks
  216.                 ON `pickware_erp_warehouse_stock`.`product_id` = newWarehouseStocks.productId
  217.                 AND `pickware_erp_warehouse_stock`.`product_version_id` = newWarehouseStocks.productVersionId
  218.                 AND `pickware_erp_warehouse_stock`.`warehouse_id` = newWarehouseStocks.warehouseId
  219.                 SET `pickware_erp_warehouse_stock`.quantity = COALESCE(newWarehouseStocks.`quantity`, 0)
  220.                 WHERE `pickware_erp_warehouse_stock`.`product_id` IN (:productIds)',
  221.                 [
  222.                     'productIds' => array_map('hex2bin'$productIds),
  223.                     'liveVersionId' => hex2bin(Defaults::LIVE_VERSION),
  224.                     'warehouseStockRelevantStockLocationTypes' => [
  225.                         LocationTypeDefinition::TECHNICAL_NAME_WAREHOUSE,
  226.                         LocationTypeDefinition::TECHNICAL_NAME_BIN_LOCATION,
  227.                     ],
  228.                 ],
  229.                 [
  230.                     'productIds' => Connection::PARAM_STR_ARRAY,
  231.                     'warehouseStockRelevantStockLocationTypes' => Connection::PARAM_STR_ARRAY,
  232.                 ],
  233.             );
  234.         });
  235.     }
  236. }