diff --git a/Model/GetInventoryRequestFromOrder.php b/Model/GetInventoryRequestFromOrder.php new file mode 100644 index 0000000..7f49eb3 --- /dev/null +++ b/Model/GetInventoryRequestFromOrder.php @@ -0,0 +1,124 @@ +inventoryRequestFactory = $inventoryRequestFactory; + $this->inventoryRequestExtensionFactory = $inventoryRequestExtensionFactory; + $this->orderRepository = $orderRepository; + $this->addressInterfaceFactory = $addressInterfaceFactory; + $this->storeManager = $storeManager; + $this->stockByWebsiteIdResolver = $stockByWebsiteIdResolver; + } + + /** + * Same as GetInventoryRequestFromOrder, but takes an order instead of an order id + * because in this scenario the order has not been saved yet. + * + * @param OrderInterface $order + * @param array $requestItems + * @return InventoryRequestInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function execute(OrderInterface $order, array $requestItems): InventoryRequestInterface + { + $store = $this->storeManager->getStore($order->getStoreId()); + $stock = $this->stockByWebsiteIdResolver->execute((int)$store->getWebsiteId()); + + $inventoryRequest = $this->inventoryRequestFactory->create([ + 'stockId' => $stock->getStockId(), + 'items' => $requestItems + ]); + + $address = $this->getAddressFromOrder($order); + if ($address !== null) { + $extensionAttributes = $this->inventoryRequestExtensionFactory->create(); + $extensionAttributes->setDestinationAddress($address); + $inventoryRequest->setExtensionAttributes($extensionAttributes); + } + + return $inventoryRequest; + } + + /** + * Create an address from an order + * + * @param OrderInterface $order + * @return null|AddressInterface + */ + private function getAddressFromOrder(OrderInterface $order): ?AddressInterface + { + /** @var Address $shippingAddress */ + $shippingAddress = $order->getShippingAddress(); + if ($shippingAddress === null) { + return null; + } + + return $this->addressInterfaceFactory->create([ + 'country' => $shippingAddress->getCountryId(), + 'postcode' => $shippingAddress->getPostcode(), + 'street' => implode("\n", $shippingAddress->getStreet()), + 'region' => $shippingAddress->getRegion() ?? $shippingAddress->getRegionCode() ?? '', + 'city' => $shippingAddress->getCity() + ]); + } +} diff --git a/Model/GetSourceSelectionResultFromOrder.php b/Model/GetSourceSelectionResultFromOrder.php new file mode 100644 index 0000000..8b2a42a --- /dev/null +++ b/Model/GetSourceSelectionResultFromOrder.php @@ -0,0 +1,124 @@ +itemRequestFactory = $itemRequestFactory; + $this->getDefaultSourceSelectionAlgorithmCode = $getDefaultSourceSelectionAlgorithmCode; + $this->sourceSelectionService = $sourceSelectionService; + $this->getSkuFromOrderItem = $getSkuFromOrderItem; + $this->getInventoryRequestFromOrder = $getInventoryRequestFromOrder ?: + ObjectManager::getInstance()->get(GetInventoryRequestFromOrder::class); + } + + /** + * @param OrderInterface $order + * @return SourceSelectionResultInterface + */ + public function execute(OrderInterface $order): SourceSelectionResultInterface + { + /** @var OrderInterface $order */ + $inventoryRequest = $this->getInventoryRequestFromOrder->execute( + $order, + $this->getSelectionRequestItems($order->getItems()) + ); + + $selectionAlgorithmCode = $this->getDefaultSourceSelectionAlgorithmCode->execute(); + return $this->sourceSelectionService->execute($inventoryRequest, $selectionAlgorithmCode); + } + + /** + * Get selection request items + * + * @param OrderItemInterface[]|Traversable $orderItems + * @return array + */ + private function getSelectionRequestItems(iterable $orderItems): array + { + $selectionRequestItems = []; + foreach ($orderItems as $orderItem) { + if ($orderItem->isDummy()) { + continue; + } + + $itemSku = $this->getSkuFromOrderItem->execute($orderItem); + $qty = $this->castQty($orderItem, $orderItem->getQtyOrdered()); + + $selectionRequestItems[] = $this->itemRequestFactory->create([ + 'sku' => $itemSku, + 'qty' => $qty, + ]); + } + return $selectionRequestItems; + } + + /** + * Cast qty value + * + * @param OrderItemInterface $item + * @param string|int|float $qty + * @return float + */ + private function castQty(OrderItemInterface $item, $qty): float + { + if ($item->getIsQtyDecimal()) { + $qty = (float) $qty; + } else { + $qty = (int) $qty; + } + + return $qty > 0 ? $qty : 0; + } +} diff --git a/Observer/CancelOrderItemObserver.php b/Observer/CancelOrderItemObserver.php index 4ce66d2..e1bb97a 100644 --- a/Observer/CancelOrderItemObserver.php +++ b/Observer/CancelOrderItemObserver.php @@ -151,33 +151,30 @@ public function execute(EventObserver $observer): void $order = $orderItem->getOrder(); - // Purposely check for single source mode first - // If your store's configuration for inventory is not single source, then you'll need something to make source - // calculation on order placement, rather than order shipment - $sourceCode = null; + // Source selection happens by default on shipment, so only shipments contain information about the + // inventory source. + // @TODO add support for multi source stock replenishment on cancellation if ($this->isSingleSourceMode->execute()) { $sourceCode = $this->defaultSourceProvider->getCode(); - } elseif (!empty($order->getExtensionAttributes()) && !empty($order->getExtensionAttributes()->getSourceCode())) { - $sourceCode = $order->getExtensionAttributes()->getSourceCode(); - } - $itemsToDeduct = []; - foreach ($itemsToCancel as $itemToCancel) { - $itemsToDeduct[] = $this->itemToDeductFactory->create([ - 'sku' => $itemToCancel->getSku(), - 'qty' => -$itemToCancel->getQuantity(), + $itemsToDeduct = []; + foreach ($itemsToCancel as $itemToCancel) { + $itemsToDeduct[] = $this->itemToDeductFactory->create([ + 'sku' => $itemToCancel->getSku(), + 'qty' => -$itemToCancel->getQuantity(), + ]); + } + + $sourceDeductionRequest = $this->sourceDeductionRequestFactory->create([ + 'sourceCode' => $sourceCode, + 'items' => $itemsToDeduct, + 'salesChannel' => $salesChannel, + 'salesEvent' => $salesEvent ]); - } - $sourceDeductionRequest = $this->sourceDeductionRequestFactory->create([ - 'sourceCode' => $sourceCode, - 'items' => $itemsToDeduct, - 'salesChannel' => $salesChannel, - 'salesEvent' => $salesEvent - ]); - - $this->sourceDeductionService->execute($sourceDeductionRequest); + $this->sourceDeductionService->execute($sourceDeductionRequest); - $this->priceIndexer->reindexRow($orderItem->getProductId()); + $this->priceIndexer->reindexRow($orderItem->getProductId()); + } } } diff --git a/Observer/SourceDeductionProcessor.php b/Observer/SourceDeductionProcessor.php index 651c5ef..c3d8d40 100644 --- a/Observer/SourceDeductionProcessor.php +++ b/Observer/SourceDeductionProcessor.php @@ -3,51 +3,46 @@ namespace Ampersand\DisableStockReservation\Observer; -use Ampersand\DisableStockReservation\Model\GetItemsToDeductFromOrder; -use Ampersand\DisableStockReservation\Model\SourceDeductionRequestFromOrderFactory; +use Ampersand\DisableStockReservation\Model\GetSourceSelectionResultFromOrder; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Event\Observer as EventObserver; -use Magento\InventorySourceDeductionApi\Model\SourceDeductionServiceInterface; -use Magento\InventoryCatalogApi\Api\DefaultSourceProviderInterface; -use Magento\InventoryCatalogApi\Model\IsSingleSourceModeInterface; -use Magento\InventorySalesApi\Api\PlaceReservationsForSalesEventInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\InventorySalesApi\Api\Data\SalesEventInterface; +use Magento\InventorySalesApi\Api\Data\SalesEventInterfaceFactory; use Magento\InventorySourceDeductionApi\Model\SourceDeductionRequestInterface; +use Magento\InventorySourceDeductionApi\Model\SourceDeductionServiceInterface; +use Magento\InventoryShipping\Model\SourceDeductionRequestsFromSourceSelectionFactory; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; use Magento\InventorySalesApi\Api\Data\ItemToSellInterfaceFactory; +use Magento\InventorySalesApi\Api\PlaceReservationsForSalesEventInterface; -/** - * Class SourceDeductionProcessor - */ class SourceDeductionProcessor implements ObserverInterface { /** - * @var IsSingleSourceModeInterface - */ - private $isSingleSourceMode; - - /** - * @var DefaultSourceProviderInterface + * @var GetSourceSelectionResultFromOrder */ - private $defaultSourceProvider; + private $getSourceSelectionResultFromOrder; /** - * @var GetItemsToDeductFromOrder + * @var SourceDeductionServiceInterface */ - private $getItemsToDeductFromOrder; + private $sourceDeductionService; /** - * @var SourceDeductionRequestFromOrderFactory + * @var SourceDeductionRequestsFromSourceSelectionFactory */ - private $sourceDeductionRequestFromOrderFactory; + private $sourceDeductionRequestsFromSourceSelectionFactory; /** - * @var SourceDeductionServiceInterface + * @var SalesEventInterfaceFactory */ - private $sourceDeductionService; + private $salesEventFactory; /** * @var ItemToSellInterfaceFactory */ - private $itemsToSellFactory; + private $itemToSellFactory; /** * @var PlaceReservationsForSalesEventInterface @@ -55,29 +50,26 @@ class SourceDeductionProcessor implements ObserverInterface private $placeReservationsForSalesEvent; /** - * @param IsSingleSourceModeInterface $isSingleSourceMode - * @param DefaultSourceProviderInterface $defaultSourceProvider - * @param GetItemsToDeductFromOrder $getItemsToDeductFromOrder - * @param SourceDeductionRequestFromOrderFactory $sourceDeductionRequestFromOrderFactory + * @param GetSourceSelectionResultFromOrder $getSourceSelectionResultFromOrder * @param SourceDeductionServiceInterface $sourceDeductionService - * @param ItemToSellInterfaceFactory $itemsToSellFactory + * @param SourceDeductionRequestsFromSourceSelectionFactory $sourceDeductionRequestsFromSourceSelectionFactory + * @param SalesEventInterfaceFactory $salesEventFactory + * @param ItemToSellInterfaceFactory $itemToSellFactory * @param PlaceReservationsForSalesEventInterface $placeReservationsForSalesEvent */ public function __construct( - IsSingleSourceModeInterface $isSingleSourceMode, - DefaultSourceProviderInterface $defaultSourceProvider, - GetItemsToDeductFromOrder $getItemsToDeductFromOrder, - SourceDeductionRequestFromOrderFactory $sourceDeductionRequestFromOrderFactory, + GetSourceSelectionResultFromOrder $getSourceSelectionResultFromOrder, SourceDeductionServiceInterface $sourceDeductionService, - ItemToSellInterfaceFactory $itemsToSellFactory, + SourceDeductionRequestsFromSourceSelectionFactory $sourceDeductionRequestsFromSourceSelectionFactory, + SalesEventInterfaceFactory $salesEventFactory, + ItemToSellInterfaceFactory $itemToSellFactory, PlaceReservationsForSalesEventInterface $placeReservationsForSalesEvent ) { - $this->isSingleSourceMode = $isSingleSourceMode; - $this->defaultSourceProvider = $defaultSourceProvider; - $this->getItemsToDeductFromOrder = $getItemsToDeductFromOrder; - $this->sourceDeductionRequestFromOrderFactory = $sourceDeductionRequestFromOrderFactory; + $this->getSourceSelectionResultFromOrder = $getSourceSelectionResultFromOrder; $this->sourceDeductionService = $sourceDeductionService; - $this->itemsToSellFactory = $itemsToSellFactory; + $this->sourceDeductionRequestsFromSourceSelectionFactory = $sourceDeductionRequestsFromSourceSelectionFactory; + $this->salesEventFactory = $salesEventFactory; + $this->itemToSellFactory = $itemToSellFactory; $this->placeReservationsForSalesEvent = $placeReservationsForSalesEvent; } @@ -93,24 +85,22 @@ public function execute(EventObserver $observer) return; } - // Purposely check for single source mode first - // If your store's configuration for inventory is not single source, then you'll need something to make source - // calculation on order placement, rather than order shipment - $sourceCode = null; - if ($this->isSingleSourceMode->execute()) { - $sourceCode = $this->defaultSourceProvider->getCode(); - } elseif (!empty($order->getExtensionAttributes()) && !empty($order->getExtensionAttributes()->getSourceCode())) { - $sourceCode = $order->getExtensionAttributes()->getSourceCode(); - } + $sourceSelectionResult = $this->getSourceSelectionResultFromOrder->execute($order); + + /** @var SalesEventInterface $salesEvent */ + $salesEvent = $this->salesEventFactory->create([ + 'type' => SalesEventInterface::EVENT_ORDER_PLACED, + 'objectType' => SalesEventInterface::OBJECT_TYPE_ORDER, + 'objectId' => $order->getEntityId(), + ]); - $orderItems = $this->getItemsToDeductFromOrder->execute($order); + $sourceDeductionRequests = $this->sourceDeductionRequestsFromSourceSelectionFactory->create( + $sourceSelectionResult, + $salesEvent, + (int)$order->getStore()->getWebsiteId() + ); - if (!empty($orderItems)) { - $sourceDeductionRequest = $this->sourceDeductionRequestFromOrderFactory->execute( - $order, - $sourceCode, - $orderItems - ); + foreach ($sourceDeductionRequests as $sourceDeductionRequest) { $this->sourceDeductionService->execute($sourceDeductionRequest); $this->placeCompensatingReservation($sourceDeductionRequest); } @@ -125,7 +115,7 @@ private function placeCompensatingReservation(SourceDeductionRequestInterface $s { $items = []; foreach ($sourceDeductionRequest->getItems() as $item) { - $items[] = $this->itemsToSellFactory->create([ + $items[] = $this->itemToSellFactory->create([ 'sku' => $item->getSku(), 'qty' => $item->getQty() ]); @@ -136,4 +126,4 @@ private function placeCompensatingReservation(SourceDeductionRequestInterface $s $sourceDeductionRequest->getSalesEvent() ); } -} +} \ No newline at end of file diff --git a/README.md b/README.md index 1c43e91..8811e5d 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,3 @@ This module will: * Prevent stock deductions on order shipment. See disabled `inventory_sales_source_deduction_processor` observer on `sales_order_shipment_save_after` event. * Replenish stock for cancelled order items. See `inventory` observer on `sales_order_item_cancel` event. -## Limitations - -Magento will determine which inventory source from where to decrement the stock on order shipment. If your store is single source -then this module will work perfectly for you. However, if your store is multi source, then you'll need to make source calculation -happen on order placement instead. See [AmpersandHQ/magento2-msi-source-on-order-placement](https://github.com/AmpersandHQ/magento2-msi-source-on-order-placement) diff --git a/composer.json b/composer.json index 48fe61e..459d911 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,3 @@ } } } - - -