Zamówienie jest w BaseLinkerze z poprawnymi danymi klienta i pozycjami, ale dostępność towaru po jego złożeniu nie maleje.
Pobierz to zamówienie przez getOrders i spójrz na pozycje: storage, storage_id, product_id, warehouse_id. Potem to samo dla zamówienia z dowolnego innego kanału w tym koncie.
Kto tu w ogóle liczy stan
W tym wdrożeniu liczby, które klient widzi na karcie i w szybkim zamawianiu, to kopia stanów z BaseLinkera, zapisana w atrybutach produktu i odświeżana co godzinę. Magento robi przy zamówieniu własną rezerwację MSI (wielomagazynowy moduł Magento), która obniża salable quantity całego źródła, nie liczby w atrybutach. Właściwe zdjęcie stanu ma nastąpić w BaseLinkerze, bo to on dostaje stany z ERP i rozdaje je do kanałów.
Dlatego pytanie „dlaczego stan nie zszedł” trzeba zadać BaseLinkerowi, nie Magento. A BaseLinker zdejmuje stan tylko wtedy, gdy pozycja zamówienia jest powiązana z produktem w jego katalogu i wskazuje magazyn.
Trzy pojęcia, które w tym tekście trzeba rozróżniać, bo w rozmowach wszystkie nazywa się „stanem”: stan magazynowy, czyli liczba sztuk w magazynie; rezerwacja, którą BaseLinker zakłada po pobraniu zamówienia, jeśli katalog ma włączony moduł rezerwacji; dostępność, czyli stan pomniejszony o rezerwacje. Obniżenie stanu magazynowego następuje w momencie realizacji zamówienia, a ten moment ustawia się w BaseLinkerze w ustawieniach przetwarzania zamówień. W tym wdrożeniu katalog ma rezerwacje włączone i to one decydują o tym, co klient B2B widzi jako dostępne.
Przyczyna 1: pozycje bez powiązania z katalogiem
Pierwszy sygnał był widoczny w panelu BaseLinkera bez żadnego API: w zamówieniach z Magento kolumna „ID produktu” przy pozycjach była pusta. Eksport wysyłał pozycje jako wolny tekst: storage: "db", storage_id: 0, product_id: "". Nazwa, SKU, cena i ilość były poprawne, więc dla człowieka zamówienie wyglądało kompletnie. Dla BaseLinkera nie było czego zdjąć.
Naprawa wymaga odnalezienia produktu w katalogu BaseLinkera i wysłania jego identyfikatora na pozycji. Produkty odnajdują się po EAN albo SKU. Żeby eksport nie odpytywał ciężkiego API przy każdym zamówieniu, identyfikator warto zapisać w atrybucie produktu przy godzinnej synchronizacji stanów, bo odpowiedź BaseLinkera i tak go zawiera.
Przyczyna 2: magazyn jest na pozycji, nie na zamówieniu
Po powiązaniu pozycji stan nadal schodził z niewłaściwego miejsca. Sklep ma dwa magazyny w jednym katalogu: własny i dostawcy. Dokumentacja addOrder mówi wprost: pole warehouse_id pominięte albo równe zeru oznacza magazyn domyślny ustalony według źródła zamówienia. Dla naszego źródła tym domyślnym okazał się magazyn dostawcy, więc każda pozycja bez wskazanego magazynu schodziła właśnie stamtąd.
Na pierwszy rzut oka wygląda to tak, jakby addOrder nie miał pola na magazyn, bo storage i storage_id wskazują źródło katalogu, a nie miejsce zdjęcia stanu. Rozstrzygają to zamówienia z innych kanałów w tym samym koncie: getOrders zwraca na każdej pozycji pole warehouse_id, i tam było ono wypełnione realnym magazynem. Nasze pozycje miały warehouse_id: 0.
| Pole pozycji | Nasz eksport | Pozycje działających kanałów w getOrders | Co przyjmuje addOrder |
|---|---|---|---|
storage | "db" | "db" | "db" dla katalogu BaseLinkera |
storage_id | 0 | identyfikator katalogu | 0 dla produktu z katalogu BaseLinkera |
product_id | pusty | identyfikator produktu w katalogu | identyfikator produktu w katalogu; pusty, gdy nieznany |
warehouse_id | 0 | identyfikator magazynu własnego albo dostawcy | magazyn; 0 lub brak to magazyn domyślny według źródła zamówienia |
Uwaga do tabeli: storage_id: 0 nie był błędem, bo tak dokumentacja addOrder oznacza produkt z katalogu BaseLinkera, a getOrders zwraca w tym polu identyfikator katalogu. Odpowiedź API i parametry wejściowe to dwa kontrakty. O zdjęciu stanu decydowały product_id i warehouse_id.
Skąd eksport ma wiedzieć, który magazyn
Klient B2B nie wybiera magazynu. Wybiera termin wysyłki: „24h” albo „3–7 dni”, przy dodawaniu produktu do koszyka. Termin odpowiada jednemu z magazynów, więc to on jest kluczem. Trzeba go tylko przenieść z koszyka na pozycję zamówienia, bo Magento nie kopiuje opcji dodatkowych pozycji koszyka na zamówienie samo z siebie; sprawdziliśmy to w wersji 2.4.8.
Gdy termin jest zapisany na pozycji, eksport wybiera magazyn jednoznacznie, a w atrybutach pozycji w BaseLinkerze zostaje opis w rodzaju „Rozmiar: D38 | Wysyłka: 24h”, więc osoba kompletująca od razu widzi, skąd brać towar. Ten sam rozmiar zamówiony z dwóch terminów to dwie osobne pozycje, każda ze swoim magazynem.
Test na jednym zamówieniu i podwójne liczenie, które wykrył
Test był prosty: jedno zamówienie testowe, ten sam rozmiar w dwóch pozycjach, po jednej sztuce z terminu „24h” i z „3–7 dni”. Stany odczytane przez getInventoryProductsStock przed i po.
| Magazyn | Przed | Po złożeniu | Pozycja w BaseLinkerze |
|---|---|---|---|
| własny (termin 24h) | 1 | 0 | product_id wypełniony, warehouse_id własny |
| dostawcy (termin 3–7 dni) | 6 | 5 | product_id wypełniony, warehouse_id dostawcy |
Zdjęcie stanu zadziałało dokładnie po jednej sztuce z każdego magazynu. Ten sam test wykrył problem po stronie Magento: lokalny mechanizm, który od razu po zamówieniu odejmuje sztuki z kopii stanu w atrybutach, iterował po wszystkich pozycjach zamówienia, a produkt konfigurowalny to dwa wiersze, rodzic i rozmiar. Każda sztuka schodziła lokalnie dwa razy (6 do 4 zamiast 6 do 5). Naprawa to pominięcie wierszy, które mają rodzica.
Otwarty problem: dostępność nie wraca po anulowaniu
Dwa tygodnie później test kontrolny pokazał, że zdejmowanie działa, ale zwrot przy anulowaniu nie. Zamówienie anulowane w Magento i przestawione w BaseLinkerze na status anulowania zostawiało towar zdjęty. Liczby w tabeli to wartość zwracana przez getInventoryProductsStock dla magazynu własnego, czyli to, co w tym katalogu z włączonymi rezerwacjami widzi sprzedaż jako dostępne. W pomiarze nie rozdzielaliśmy stanu fizycznego od rezerwacji.
| Moment | Wartość |
|---|---|
| przed zamówieniem | 31 |
| po złożeniu | 30 |
| po anulowaniu w Magento i statusie anulowania w BaseLinkerze | 30 |
| po zmianie na systemowy status „Anulowane” | 30 |
po usunięciu pozycji zamówienia przez deleteOrderProduct | 31 |
Co odczytaliśmy: żadna ze sprawdzonych zmian statusu, własny status anulowania i systemowy „Anulowane”, nie przywróciła dostępności. Usunięcie pozycji zamówienia przez deleteOrderProduct przywróciło ją natychmiast, co zgadza się z opisem rezerwacji w pomocy Base: zmiana liczby zakupionych sztuk w zamówieniu zmienia rezerwację w katalogu. W API nie znaleźliśmy pola, które wiązałoby status ze zwrotem stanu; addOrderStatus ma tylko nazwę, kolor, typ do analityki, grupę i komentarz.
Czego nie potwierdziliśmy: Base ma osobną operację anulowania zamówienia, z karty zamówienia albo akcją automatyczną, różną od przeniesienia zamówienia do statusu o nazwie „Anulowane”. Tej ścieżki nie mierzyliśmy, podobnie jak wpływu ustawień przetwarzania zamówień na moment realizacji. To jest następny krok diagnozy, nie wniosek.
Kiedy zrobisz to sam, a kiedy nie
Diagnoza: pusta kolumna „ID produktu”, porównanie pozycji z innym kanałem przez getOrders, sprawdzenie magazynu domyślnego i rezerwacji w katalogu. Jeśli eksport robi gotowa wtyczka, z tą wiedzą zgłosisz dostawcy konkretny błąd zamiast „nie działa”.
Własny eksport zamówień: powiązanie pozycji z katalogiem bez odpytywania API przy każdym zamówieniu, magazyn wybierany z danych pozycji, test na jednym zamówieniu z odczytem stanów i rezerwacji przed i po, ścieżka anulowania sprawdzona w panelu przed jakimkolwiek automatem, ponawianie nieudanych eksportów bez duplikatów.
Jedno zdanie o kolejności: najpierw powiązanie pozycji, potem magazyn, na końcu anulowanie. W odwrotnej kolejności nie da się niczego zmierzyć, bo bez powiązania stan nie rusza się wcale. Jak podzielić odpowiedzialność za stany między ERP, BaseLinker i sklep, opisuje poradnik Jak synchronizować stany między ERP, sklepem i BaseLinkerem. Jak ponawiać eksport zamówienia, który odbił się od limitu API, i jak ułożyć crony sklepu, gdy limit dzielisz z ERP, opisujemy w poradniku ERROR_BLOCKED_TOKEN i limit API BaseLinkera.