PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

MSI – Source, Stock, SSA, własny algorytm, migracja ze starego API

by Henryk Tews / wtorek, 11 października 2022 / Opublikowano w Magento 2

Multi Source Inventory (MSI) to moduł wprowadzony w Magento 2.3, który całkowicie zmienia sposób zarządzania stanami magazynowymi. Zamiast jednego globalnego stanu dla każdego SKU, masz wiele źródeł (magazynów fizycznych, sklepów, dostawców) i algorytmy wyboru z którego źródła realizować zamówienie. Jeśli rozwijasz moduły Magento i jeszcze nie poznałeś MSI – ten wpis jest dla Ciebie.

Stary model vs MSI – czym się różnią

Przed MSI Magento miało jeden stan magazynowy per produkt per strona (website). Tabela cataloginventory_stock_item trzymała jedną ilość dla każdego SKU. Proste, ale nieelastyczne – nie dało się modelować wielu magazynów fizycznych ani różnych strategii realizacji.

MSI wprowadza trzy nowe pojęcia:

  • Source (Źródło) – fizyczna lokalizacja zapasów (magazyn, sklep stacjonarny, dostawca)
  • Stock (Zasób) – wirtualna pula zapasów agregująca wiele Sources, przypisana do kanałów sprzedaży (websites)
  • Source Selection Algorithm – algorytm decydujący z którego Source realizować konkretne zamówienie

Konfiguracja Source i Stock

# Sprawdź dostępne source'y przez CLI
bin/magento inventory:source:list

# Sprawdź skonfigurowane stocki
bin/magento inventory:stock:list

Przez kod możesz zarządzać Source i Stock programatycznie:

<?php

declare(strict_types=1);

use Magento\InventoryApi\Api\Data\SourceInterfaceFactory;
use Magento\InventoryApi\Api\SourceRepositoryInterface;

class SourceManager
{
    public function __construct(
        private SourceInterfaceFactory $sourceFactory,
        private SourceRepositoryInterface $sourceRepository
    ) {}

    public function createSource(
        string $code,
        string $name,
        string $city,
        string $countryId = 'PL'
    ): void {
        $source = $this->sourceFactory->create();
        $source->setSourceCode($code);
        $source->setName($name);
        $source->setEnabled(true);
        $source->setCity($city);
        $source->setCountryId($countryId);
        $source->setPostcode('00-001');

        $this->sourceRepository->save($source);
    }
}

Zarządzanie ilościami w Source

<?php

declare(strict_types=1);

use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory;
use Magento\InventoryApi\Api\SourceItemsSaveInterface;
use Magento\InventoryApi\Api\Data\SourceItemInterface;

class InventoryManager
{
    public function __construct(
        private SourceItemInterfaceFactory $sourceItemFactory,
        private SourceItemsSaveInterface $sourceItemsSave
    ) {}

    public function updateQuantity(
        string $sku,
        string $sourceCode,
        float $quantity
    ): void {
        $sourceItem = $this->sourceItemFactory->create();
        $sourceItem->setSku($sku);
        $sourceItem->setSourceCode($sourceCode);
        $sourceItem->setQuantity($quantity);
        $sourceItem->setStatus(
            $quantity > 0
                ? SourceItemInterface::STATUS_IN_STOCK
                : SourceItemInterface::STATUS_OUT_OF_STOCK
        );

        // Zapis wielu source itemów naraz - bardziej wydajne
        $this->sourceItemsSave->execute([$sourceItem]);
    }

    public function bulkUpdate(array $updates): void
    {
        // $updates = [['sku' => 'SKU-001', 'source' => 'warehouse-warsaw', 'qty' => 50], ...]
        $sourceItems = [];

        foreach ($updates as $update) {
            $item = $this->sourceItemFactory->create();
            $item->setSku($update['sku']);
            $item->setSourceCode($update['source']);
            $item->setQuantity((float) $update['qty']);
            $item->setStatus(
                $update['qty'] > 0
                    ? SourceItemInterface::STATUS_IN_STOCK
                    : SourceItemInterface::STATUS_OUT_OF_STOCK
            );

            $sourceItems[] = $item;
        }

        $this->sourceItemsSave->execute($sourceItems);
    }
}

Pobieranie dostępności produktu

<?php

declare(strict_types=1);

use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;
use Magento\InventorySalesApi\Api\IsProductSalableInterface;
use Magento\InventorySalesApi\Api\Data\SalesChannelInterface;
use Magento\InventorySalesApi\Api\Data\SalesChannelInterfaceFactory;
use Magento\InventorySalesApi\Api\StockResolverInterface;

class AvailabilityChecker
{
    public function __construct(
        private GetProductSalableQtyInterface $getProductSalableQty,
        private IsProductSalableInterface $isProductSalable,
        private StockResolverInterface $stockResolver,
        private SalesChannelInterfaceFactory $salesChannelFactory
    ) {}

    public function getSalableQty(string $sku, string $websiteCode = 'base'): float
    {
        $salesChannel = $this->salesChannelFactory->create([
            'data' => [
                'type' => SalesChannelInterface::TYPE_WEBSITE,
                'code' => $websiteCode,
            ],
        ]);

        $stock   = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);
        $stockId = (int) $stock->getStockId();

        return $this->getProductSalableQty->execute($sku, $stockId);
    }

    public function isSalable(string $sku, string $websiteCode = 'base'): bool
    {
        $stock   = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);
        $stockId = (int) $stock->getStockId();

        return $this->isProductSalable->execute($sku, $stockId);
    }
}

Source Selection Algorithm – wybór źródła realizacji

Gdy zamówienie jest składane, MSI musi zdecydować z którego Source pobrać towar. Dostępne algorytmy:

  • Priority based – pobiera z Source o najwyższym priorytecie (domyślny)
  • Distance based – pobiera z Source najbliższego adresu dostawy (wymaga Google Maps API)

Własny algorytm SSA:

<?php

declare(strict_types=1);

use Magento\InventorySourceSelectionApi\Api\Data\InventoryRequestInterface;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionResultInterface;
use Magento\InventorySourceSelectionApi\Api\SourceSelectionServiceInterface;
use Magento\InventorySourceSelectionApi\Model\SourceSelectionInterface;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionResultInterfaceFactory;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionItemInterfaceFactory;

class CheapestSourceAlgorithm implements SourceSelectionInterface
{
    // Unikalny kod algorytmu - rejestrowany w di.xml
    public const CODE = 'vendor_cheapest_source';

    public function __construct(
        private SourceSelectionResultInterfaceFactory $sourceSelectionResultFactory,
        private SourceSelectionItemInterfaceFactory $sourceSelectionItemFactory
    ) {}

    public function execute(InventoryRequestInterface $inventoryRequest): SourceSelectionResultInterface
    {
        $items = [];

        foreach ($inventoryRequest->getItems() as $item) {
            // Tu twoja logika wyboru najtańszego źródła
            // np. na podstawie kosztu wysyłki z każdego source
            $sourceCode = $this->findCheapestSource($item->getSku(), $item->getQty());

            $items[] = $this->sourceSelectionItemFactory->create([
                'sourceCode'       => $sourceCode,
                'sku'              => $item->getSku(),
                'qtyToDeduct'      => $item->getQty(),
                'qtyAvailable'     => $this->getQtyAvailable($sourceCode, $item->getSku()),
            ]);
        }

        return $this->sourceSelectionResultFactory->create([
            'sourceSelectionItems' => $items,
            'isShippable'          => !empty($items),
        ]);
    }

    private function findCheapestSource(string $sku, float $qty): string
    {
        // Logika biznesowa - np. porównanie kosztów wysyłki
        return 'warehouse-warsaw'; // uproszczenie
    }

    private function getQtyAvailable(string $sourceCode, string $sku): float
    {
        return 100.0; // uproszczenie
    }
}

Rejestracja algorytmu w di.xml:

<?xml version="1.0"?>
<config>
    <type name="Magento\InventorySourceSelectionApi\Model\SourceSelectionService">
        <arguments>
            <argument name="sourceSelectionMethods" xsi:type="array">
                <item name="vendor_cheapest_source" xsi:type="object">
                    Vendor\Module\Model\Algorithm\CheapestSourceAlgorithm
                </item>
            </argument>
        </arguments>
    </type>
</config>

Reindeksacja MSI

# MSI ma własne indeksery
bin/magento indexer:reindex inventory

# Sprawdź stan indekserów MSI
bin/magento indexer:status | grep inventory

# Przelicz rezerwacje (ważne po imporcie zamówień)
bin/magento inventory:reservations:cleanup

Pułapki przy migracji na MSI

Jeśli masz stary moduł który bezpośrednio operuje na cataloginventory_stock_item przez Magento\CatalogInventory\Api\StockRegistryInterface – to nadal działa, ale operuje tylko na „Default Source”. Żeby w pełni korzystać z MSI musisz migrować do interfejsów z przestrzeni Magento\InventoryApi.

<?php

// STARY sposób - działa tylko z Default Source
use Magento\CatalogInventory\Api\StockRegistryInterface;

$stockItem = $stockRegistry->getStockItemBySku('SKU-001');
$qty = $stockItem->getQty(); // tylko Default Source!

// NOWY sposób - uwzględnia wszystkie Sources w Stocku
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;

$salableQty = $getProductSalableQty->execute('SKU-001', $stockId); // sumuje wszystkie Sources

Podsumowanie

MSI to fundamentalna zmiana w zarządzaniu inwentarzem Magento – od prostego licznika do elastycznego systemu wielomagazynowego. Dla projektów które mają jeden magazyn fizyczny różnica jest nieodczuwalna. Prawdziwa wartość pojawia się przy wielu lokalizacjach, dropshippingu lub click-and-collect. Kluczowe przy pracy z MSI to używanie nowych interfejsów z Magento\InventoryApi zamiast starych z Magento\CatalogInventory.

About Henryk Tews

Co możesz przeczytać następne

Hyvä vs Luma benchmark – twarde liczby, LCP 3.5x szybszy, k6 load test, konwersja
Własny indekser – mview.xml, flat table, full/partial reindeksacja, triggerowane przez cron
Optymalizacja wydajności – OPcache, Redis, N+1 queries, Blackfire, tabela priorytetów
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}