ADOBE COMMERCE

Integrating Adobe Commerce Inventory with external ERP Systems

Oleg Blinnikov

Integrating Adobe Commerce Multi Source Inventory with external ERP Systems Adobe Commerce MSI Multi Source Inventory

Multi-Source Inventory (MSI) project is designed to enable stock management in multiple locations so that merchants can properly reflect their physical inventory in Magento without having to use extensions or customization.

MSI is a fully community driven project and was delivered to the Magento Core starting from version 2.3

Since many years, we worked on a lot of different integrations which used multi-store inventory and now I want to share success story of one of the most challenging one.

In one of the projects we faced situation, when all stock data have to be loaded from the external system and we had to manage all the sales processes on Adobe Commerce without saving Stock information.

So let's have a look at such integration from technical point of view.

Magento 2.4.2 compatible syntax is used to show the examples of code. The protocol of the interaction with ERP is REST / JSON API, a two-way data exchange.

First of all, it is necessary to determine the functionality and set of requirements for the system:

In the context of this article, we omit the explanations to the components that are not directly related to the topic at hand.

Implement Stock API service for receiving stock data from ERP

There are several extension points of MSI, which we can use to achieve external stock service integration.

First of all, there are two conditions chains exists in Magento, which are:

/**
 * Service which detects whether Product is salable for a given Stock (stock data + reservations)
 **/
 Magento\InventorySales\Model\IsProductSalableCondition\IsProductSalableConditionChain
/**
  * Service which detects whether a certain Qty of Product is salable for a given Stock (stock data + reservations)
 **/
 Magento\InventorySales\Model\IsProductSalableForRequestedQtyCondition\IsProductSalableForRequestedQtyConditionChain

First of all, there are two conditions chains exists in Magento, which are:

 <type name="Magento\InventorySales\Model\IsProductSalableCondition\IsProductSalableConditionChain">
        <arguments>
             <argument name="conditions" xsi:type="array">
                 <item name="stock_api_is_in_stock" xsi:type="array">
                     <item name="sort_order" xsi:type="number">90</item>
                     <item name="object" xsi:type="object">Comwrap\ProductStock\Model\IsProductSalableCondition</item>
                 </item>
             </argument>
         </arguments>
     </type>
     <type name="Magento\InventorySales\Model\IsProductSalableForRequestedQtyCondition\IsProductSalableForRequestedQtyConditionChain">
         <arguments>
             <argument name="conditions" xsi:type="array">
                 <item name="stock_api_is_correct_qty" xsi:type="array">
                     <item name="sort_order" xsi:type="number">90</item>
                     <item name="object" xsi:type="object">Comwrap\ProductStock\Model\IsCorrectQtyConditionApi</item>
                 </item>
             </argument>
         </arguments>
     </type>

In implementations we creating classes which implements

Magento\InventorySalesApi\Api\IsProductSalableInterface

and

Magento\InventorySalesApi\Api\IsProductSalableForRequestedQtyInterface

When this article was published, those two interfaces are already marked as deprecated.
So please check Magento\InventorySalesApi\Api\AreProductsSalableInterface and Magento\InventorySalesApi\Api\AreProductsSalableForRequestedQtyInterface

In execute(string $sku, int $stockId) and execute(string $sku, int $stockId, float $requestedQty): ProductSalableResultInterface methods you have to add implementation of your external source.

Technically, it does not really matter where the data comes from, you can add external service, ERP, *.csv or database as source of truth.

Disable other conditions in chain

In our case we do not want to have any dependency on other conditions in the chain, so we removed them and made a fallback in case if feature is disabled.

<?xml version="1.0"?>
 <!--
 /**
  * Copyright © Magento, Inc. All rights reserved.
  * See COPYING.txt for license details.
  */
 -->
 <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
     <preference for="Magento\InventorySales\Model\IsProductSalableCondition\IsProductSalableConditionChain"
                 type="Comwrap\ProductStock\Model\IsProductSalableCondition\IsProductSalableConditionChain" />
     <preference for="IsProductSalableForRequestedQtyConditionChainOnAddToCart"
                 type="Comwrap\ProductStock\Model\IsProductSalableForRequestedQtyCondition\IsProductSalableForRequestedQtyConditionChain" />
 </config>

This action is not required, all depends on your requirements.

The new chain file:

<?php
 declare(strict_types=1);

 namespace Comwrap\ProductStock\Model\IsProductSalableCondition;

 use Magento\InventorySales\Model\IsProductSalableCondition\IsProductSalableConditionChain as ConditionChain;

 /**
  * Class IsProductSalableConditionChain
  * Overrides Condition Chain to work only with one condition
  */
 class IsProductSalableConditionChain extends ConditionChain
 {
     /**
      * @var array
      */
     private $condition;

     /**
      * @param array $conditions
      */
     public function __construct(
         array $conditions
     ) {
         if (isset($conditions['stock_api_is_in_stock'])) {
             $this->condition['stock_api_is_in_stock'] = $conditions['stock_api_is_in_stock'];
         } else {
             $this->condition = $conditions;
         }
         parent::__construct($this->condition);
     }
 }

Update Product Qty

One more change we need to make, is to set QTY for product while requesting the stock.

This one can be achieved by Plugin on Magento\InventorySales\Model\GetProductSalableQty class:

<type name="Magento\InventorySales\Model\GetProductSalableQty">
        <plugin sortOrder="1" name="comwrapProductStockGetProductSalableQty"
                type="Comwrap\ProductStock\Plugin\Model\GetProductSalableQtyPlugin"/>
    </type>

Comwrap\ProductStock\Plugin\Model\GetProductSalableQtyPlugin:

<?php
declare(strict_types=1);

namespace Comwrap\ProductStock\Plugin\Model;

use Comwrap\ProductStock\Model\ProductStock;
use Magento\InventorySales\Model\GetProductSalableQty;

/**
 * Class GetProductSalableQtyPlugin
 * Plugin for updating salable qty of product
 */
class GetProductSalableQtyPlugin
{
    /**
     * @var ProductStock
     */
    private $productStock;

    /**
     * GetProductSalableQtyPlugin constructor.
     * @param ProductStock $productStock
     */
    public function __construct(
        ProductStock $productStock
    ) {
        $this->productStock = $productStock;
    }

    /**
     * @param GetProductSalableQty $subject
     * @param callable $proceed
     * @param string $sku
     * @param int $stockId
     * @return float
     * @noinspection PhpUnusedParameterInspection
     */
    public function aroundExecute(GetProductSalableQty $subject, callable $proceed, string $sku, int $stockId): float
    {
        $result = $proceed($sku, $stockId);
        return $this->productStock->getProductSalableQty($sku);
    }
}

After those basic extensions to existing mechanisms, you already can replace stock source from custom location.

Conclusion

Magento MSI module was one of the first modules which followed service decomposition approach. At the same time it was developed in way, to give developers opportunity extend it in an easiest way possible. At the beginning of our project, we expected much more work to do, but after digging into the module architecture, all our changes were made only by using extension points.

You still need to take care about some minor tasks as integration of API connection to external system, or modifying templates in case if your stock is changing very often and you do not want to clean PDP/Category caches all the time this happen.

Photo by Martin Adams on Unsplash