Skip to content

Commit

Permalink
Merge pull request #82 from AmpersandHQ/8-creditmemo-back-to-stock-am…
Browse files Browse the repository at this point in the history
…persandcopy

Back in stock, when credit memo creation occours
  • Loading branch information
convenient authored Oct 4, 2022
2 parents 4a6f50a + 58d4633 commit 6c6776d
Show file tree
Hide file tree
Showing 5 changed files with 358 additions and 1 deletion.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AmpersandHQ/magento2-disable-stock-reservation

[![Build Status](https://travis-ci.com/AmpersandHQ/magento2-disable-stock-reservation.svg?branch=master)](https://travis-ci.com/AmpersandHQ/magento2-disable-stock-reservation)
[![Build Status](https://app.travis-ci.com/AmpersandHQ/magento2-disable-stock-reservation.svg?branch=master)](https://app.travis-ci.com/AmpersandHQ/magento2-disable-stock-reservation)

This module disables the inventory reservation logic introduced as part of MSI in Magento 2.3.3 - see
https://github.com/magento/inventory/issues/2269 for more information about the way MSI was implemented, and the issues
Expand All @@ -19,6 +19,9 @@ This module will:
* Trigger stock deductions on order placement. See `inventory_sales_source_deduction_processor` plugin on `Magento\Sales\Model\Service\OrderService`.
* 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.
* Replenish stock when a credit memo is issued. See `src/Observer/RestoreSourceItemQuantityOnRefundObserver.php`
* Requires that "Back to stock" is checked or "Automatically Return Credit Memo Item to Stock" is configured
* https://docs.magento.com/user-guide/configuration/catalog/inventory.html#product-stock-options

## Additional Notes

Expand Down
255 changes: 255 additions & 0 deletions src/Observer/RestoreSourceItemQuantityOnRefundObserver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<?php

namespace Ampersand\DisableStockReservation\Observer;

use Ampersand\DisableStockReservation\ReturnProcessor\GetSalesChannelForOrder;
use Ampersand\DisableStockReservation\ReturnProcessor\GetSalesChannelForOrderFactory;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\InventoryApi\Api\GetSourceItemsBySkuInterface;
use Magento\InventoryApi\Api\GetSourcesAssignedToStockOrderedByPriorityInterface;
use Magento\InventoryCatalogApi\Api\DefaultSourceProviderInterface;
use Magento\InventoryCatalogApi\Model\GetProductTypesBySkusInterface;
use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForProductTypeInterface;
use Magento\InventorySalesApi\Api\Data\SalesEventExtensionFactory;
use Magento\InventorySalesApi\Api\Data\SalesEventExtensionInterface;
use Magento\InventorySalesApi\Api\Data\SalesEventInterface;
use Magento\InventorySalesApi\Api\Data\SalesEventInterfaceFactory;
use Magento\InventorySalesApi\Model\GetSkuFromOrderItemInterface;
use Magento\InventorySalesApi\Model\StockByWebsiteIdResolverInterface;
use Magento\InventorySourceDeductionApi\Model\ItemToDeductFactory;
use Magento\InventorySourceDeductionApi\Model\SourceDeductionRequestFactory;
use Magento\InventorySourceDeductionApi\Model\SourceDeductionService;
use Magento\Sales\Model\OrderRepository;

class RestoreSourceItemQuantityOnRefundObserver implements ObserverInterface
{
/**
* @var GetSkuFromOrderItemInterface
*/
private $getSkuFromOrderItem;

/**
* @var IsSourceItemManagementAllowedForProductTypeInterface
*/
private $isSourceItemManagementAllowedForProductType;

/**
* @var GetProductTypesBySkusInterface
*/
private $getProductTypesBySkus;

/**
* @var OrderRepository
*/
private $orderRepository;

/**
* @var DefaultSourceProviderInterface
*/
private $defaultSourceProvider;


/**
* @var GetSourcesAssignedToStockOrderedByPriorityInterface
*/
private $getSourcesAssignedToStockOrderedByPriority;

/**
* @var StockByWebsiteIdResolverInterface
*/
private $stockByWebsiteIdResolver;


/**
* @var SourceDeductionRequestFactory
*/
private $sourceDeductionRequestFactory;

/**
* @var SalesEventExtensionFactory;
*/
private $salesEventExtensionFactory;

/**
* @var GetSalesChannelForOrder|\Magento\InventorySales\Model\ReturnProcessor\GetSalesChannelForOrder
*/
private $getSalesChannelForOrder;

/**
* @var SourceDeductionService
*/
private $sourceDeductionService;


/**
* @var GetSourceItemsBySkuInterface
*/
private $getSourceItemsBySku;

/**
* @var SalesEventInterfaceFactory
*/
private $salesEventFactory;

/**
* @var ItemToDeductFactory
*/
private $itemToDeductFactory;

/**
* RestoreSourceItemQuantityOnRefundObserver constructor.
*
* @param GetSkuFromOrderItemInterface $getSkuFromOrderItem
* @param IsSourceItemManagementAllowedForProductTypeInterface $isSourceItemManagementAllowedForProductType
* @param GetProductTypesBySkusInterface $getProductTypesBySkus
* @param OrderRepository $orderRepository
* @param DefaultSourceProviderInterface $defaultSourceProvider
* @param GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority
* @param StockByWebsiteIdResolverInterface $stockByWebsiteIdResolver
* @param SourceDeductionRequestFactory $sourceDeductionRequestFactory
* @param SalesEventExtensionFactory $salesEventExtensionFactory
* @param GetSalesChannelForOrderFactory $getSalesChannelForOrderFactory
* @param SourceDeductionService $sourceDeductionService
* @param GetSourceItemsBySkuInterface $getSourceItemsBySku
* @param SalesEventInterfaceFactory $salesEventFactory
* @param ItemToDeductFactory $itemToDeductFactory
*/
public function __construct(
GetSkuFromOrderItemInterface $getSkuFromOrderItem,
IsSourceItemManagementAllowedForProductTypeInterface $isSourceItemManagementAllowedForProductType,
GetProductTypesBySkusInterface $getProductTypesBySkus,
OrderRepository $orderRepository,
DefaultSourceProviderInterface $defaultSourceProvider,
GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesAssignedToStockOrderedByPriority,
StockByWebsiteIdResolverInterface $stockByWebsiteIdResolver,
SourceDeductionRequestFactory $sourceDeductionRequestFactory,
SalesEventExtensionFactory $salesEventExtensionFactory,
GetSalesChannelForOrderFactory $getSalesChannelForOrderFactory,
SourceDeductionService $sourceDeductionService,
GetSourceItemsBySkuInterface $getSourceItemsBySku,
SalesEventInterfaceFactory $salesEventFactory,
ItemToDeductFactory $itemToDeductFactory
) {
$this->getSkuFromOrderItem = $getSkuFromOrderItem;


$this->isSourceItemManagementAllowedForProductType = $isSourceItemManagementAllowedForProductType;
$this->getProductTypesBySkus = $getProductTypesBySkus;
$this->orderRepository = $orderRepository;
$this->defaultSourceProvider = $defaultSourceProvider;
$this->getSourcesAssignedToStockOrderedByPriority = $getSourcesAssignedToStockOrderedByPriority;
$this->stockByWebsiteIdResolver = $stockByWebsiteIdResolver;

$this->sourceDeductionRequestFactory = $sourceDeductionRequestFactory;
$this->salesEventExtensionFactory = $salesEventExtensionFactory;
$this->getSalesChannelForOrder = $getSalesChannelForOrderFactory->create();
$this->sourceDeductionService = $sourceDeductionService;
$this->getSourceItemsBySku = $getSourceItemsBySku;
$this->salesEventFactory = $salesEventFactory;
$this->itemToDeductFactory = $itemToDeductFactory;
}


public function execute(Observer $observer)
{
/* @var $creditMemo \Magento\Sales\Model\Order\Creditmemo */
$creditMemo = $observer->getEvent()->getCreditmemo();
$order = $this->orderRepository->get($creditMemo->getOrderId());
$websiteId = (int)$order->getStore()->getWebsiteId();
$salesChannel = $this->getSalesChannelForOrder->execute($order);

$items = $returnToStockItems = [];
foreach ($creditMemo->getItems() as $item) {
$orderItem = $item->getOrderItem();
$itemSku = $this->getSkuFromOrderItem->execute($orderItem);

if ($this->isValidItem($itemSku, $orderItem->getProductType()) && $item->getBackToStock()) {
$returnToStockItems[] = $item->getOrderItemId();
$qty = $item->getQty();
$stockId = (int)$this->stockByWebsiteIdResolver->execute($websiteId)->getStockId();
$sourceCode = $this->getSourceCodeWithHighestPriorityBySku((string)$itemSku, $stockId);
$items[$sourceCode][] = $this->itemToDeductFactory->create([
'sku' => $itemSku,
'qty' => -$qty
]);
}
}

/** @var SalesEventExtensionInterface */
$salesEventExtension = $this->salesEventExtensionFactory->create([
'data' => ['objectIncrementId' => (string)$order->getIncrementId()]
]);
/** @var SalesEventInterface $salesEvent */
$salesEvent = $this->salesEventFactory->create([
'type' => SalesEventInterface::EVENT_CREDITMEMO_CREATED,
'objectType' => SalesEventInterface::OBJECT_TYPE_ORDER,
'objectId' => (string)$order->getEntityId()
]);
$salesEvent->setExtensionAttributes($salesEventExtension);

foreach ($items as $sourceCode => $items) {
$sourceDeductionRequest = $this->sourceDeductionRequestFactory->create([
'sourceCode' => $sourceCode,
'items' => $items,
'salesChannel' => $salesChannel,
'salesEvent' => $salesEvent
]);
$this->sourceDeductionService->execute($sourceDeductionRequest);
}
}

/**
* Verify is item valid for return qty to stock.
*
* @param string $sku
* @param string|null $typeId
*
* @return bool
*/
private function isValidItem(string $sku, ?string $typeId): bool
{
// https://github.com/magento-engcom/msi/issues/1761
// If product type located in table sales_order_item is "grouped" replace it with "simple"
if ($typeId === 'grouped') {
$typeId = 'simple';
}

$productType = $typeId ?: $this->getProductTypesBySkus->execute(
[$sku]
)[$sku];

return $this->isSourceItemManagementAllowedForProductType->execute($productType);
}

/**
* Returns source code with highest priority by sku
*
* @param string $sku
* @param int $stockId
*
* @return string
*/
private function getSourceCodeWithHighestPriorityBySku(string $sku, int $stockId): string
{
$sourceCode = $this->defaultSourceProvider->getCode();
try {
$availableSourcesForProduct = $this->getSourceItemsBySku->execute($sku);
$assignedSourcesToStock = $this->getSourcesAssignedToStockOrderedByPriority->execute($stockId);
foreach ($assignedSourcesToStock as $assignedSource) {
foreach ($availableSourcesForProduct as $availableSource) {
if ($assignedSource->getSourceCode() == $availableSource->getSourceCode()) {
$sourceCode = $assignedSource->getSourceCode();
break 2;
}
}
}
} catch (LocalizedException $e) {
//Use Default Source if the source can't be resolved
return $sourceCode;
}

return $sourceCode;
}
}
52 changes: 52 additions & 0 deletions src/ReturnProcessor/GetSalesChannelForOrder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Ampersand\DisableStockReservation\ReturnProcessor;

use Magento\InventorySalesApi\Api\Data\SalesChannelInterface;
use Magento\InventorySalesApi\Api\Data\SalesChannelInterfaceFactory;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Store\Api\WebsiteRepositoryInterface;

class GetSalesChannelForOrder
{
/**
* @var SalesChannelInterfaceFactory
*/
private $salesChannelFactory;

/**
* @var WebsiteRepositoryInterface
*/
private $websiteRepository;

/**
* @param WebsiteRepositoryInterface $websiteRepository
* @param SalesChannelInterfaceFactory $salesChannelFactory
*/
public function __construct(
WebsiteRepositoryInterface $websiteRepository,
SalesChannelInterfaceFactory $salesChannelFactory
) {
$this->websiteRepository = $websiteRepository;
$this->salesChannelFactory = $salesChannelFactory;
}

/**
* Return sales channel for order
*
* @param OrderInterface $order
* @return SalesChannelInterface
*/
public function execute(OrderInterface $order): SalesChannelInterface
{
$websiteId = (int)$order->getStore()->getWebsiteId();
$websiteCode = $this->websiteRepository->getById($websiteId)->getCode();

return $this->salesChannelFactory->create([
'data' => [
'type' => SalesChannelInterface::TYPE_WEBSITE,
'code' => $websiteCode
]
]);
}
}
39 changes: 39 additions & 0 deletions src/ReturnProcessor/GetSalesChannelForOrderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Ampersand\DisableStockReservation\ReturnProcessor;

use Magento\Framework\ObjectManagerInterface;
use Magento\InventorySales\Model\ReturnProcessor\GetSalesChannelForOrder as GetSalesChannelForOrder24;

class GetSalesChannelForOrderFactory
{
/**
* @var ObjectManagerInterface
*/
private $objectManager;

/**
* constructor.
*/
public function __construct(
ObjectManagerInterface $objectManager
) {
$this->objectManager = $objectManager;
}

/**
* For magento 2.4 return the core provided class
* For magento 2.3 return the workaround copy of that class
*
* @return GetSalesChannelForOrder|GetSalesChannelForOrder24|mixed
*/
public function create()
{
if (\class_exists(GetSalesChannelForOrder24::class)) {
return $this->objectManager->create(GetSalesChannelForOrder24::class);
}
return $this->objectManager->create(
GetSalesChannelForOrder::class
);
}
}
8 changes: 8 additions & 0 deletions src/etc/events.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
<event name="sales_order_shipment_save_after">
<observer name="inventory_sales_source_deduction_processor" disabled="true"/>
</event>
<event name="sales_order_creditmemo_save_after">
<!-- Disable observer that deducts stock from credit memo save -->
<observer name="deduct_source_item_quantity_on_refund" disabled="true"/>
<!-- Return stock on order credit memo creation -->
<!-- This observer is based on the magento core to restore product qty, when flag "Back in stock" is ON. -->
<observer name="add_source_item_quantity_on_refund" instance="Ampersand\DisableStockReservation\Observer\RestoreSourceItemQuantityOnRefundObserver"/>
</event>

<!-- Return stock on order cancellation -->
<!-- This observer replaces the original to avoid reindexing the price twice -->
<event name="sales_order_item_cancel">
Expand Down

0 comments on commit 6c6776d

Please sign in to comment.