Przejdź do treści

Zamówienie z Magento trafia do BaseLinkera, ale nie zmniejsza stanu: jak znaleźć przyczynę

Eksport zamówień ze sklepu B2B na Magento do BaseLinkera zgłaszał sukces, zamówienia były widoczne w panelu, a dostępność towaru stała w miejscu. Znaleźliśmy i naprawiliśmy dwie przyczyny: pozycje bez powiązania z katalogiem i brak magazynu na pozycji. Gdy dostępność zaczęła spadać po zamówieniu, test kontrolny pokazał trzeci problem: po anulowaniu nie wracała. Ten wątek opisuję do miejsca, w którym skończyły się nasze pomiary. Do tego test na jednym zamówieniu i podwójne liczenie, które ten test wykrył przy okazji.

Objaw

Zamówienie jest w BaseLinkerze z poprawnymi danymi klienta i pozycjami, ale dostępność towaru po jego złożeniu nie maleje.

Pierwszy test

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.

01

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.

02

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.

Pusta kolumna „ID produktu” w zamówieniach z jednego kanału, przy wypełnionej w pozostałych, to najszybszy test, jaki znam. Nie wymaga programisty.
03

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.

Pola pozycji zamówienia: co wysyłał eksport, co pokazują pozycje działających kanałów, co mówi dokumentacja addOrder
Pole pozycjiNasz eksportPozycje działających kanałów w getOrdersCo przyjmuje addOrder
storage"db""db""db" dla katalogu BaseLinkera
storage_id0identyfikator katalogu0 dla produktu z katalogu BaseLinkera
product_idpustyidentyfikator produktu w kataloguidentyfikator produktu w katalogu; pusty, gdy nieznany
warehouse_id0identyfikator magazynu własnego albo dostawcymagazyn; 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.

04

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.

05

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.

Stany w dwóch magazynach przed zamówieniem testowym i po nim
MagazynPrzedPo złożeniuPozycja w BaseLinkerze
własny (termin 24h)10product_id wypełniony, warehouse_id własny
dostawcy (termin 3–7 dni)65product_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.

Zasada, która została z tego testu: przy liczeniu po pozycjach zamówienia w Magento ustal, który poziom przetwarzasz, rodzica czy wariant, i nie licz tej samej zamówionej sztuki dwa razy. W naszym mechanizmie wystarczył jeden poziom; w operacji, która potrzebuje SKU i stanu konkretnego rozmiaru, właściwy jest wiersz wariantu. Ten sam błąd potrafi podwoić pozycje w mailu z potwierdzeniem.
06

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.

Wartość zwracana przez getInventoryProductsStock dla magazynu własnego na kolejnych etapach anulowania zamówienia testowego
MomentWartość
przed zamówieniem31
po złożeniu30
po anulowaniu w Magento i statusie anulowania w BaseLinkerze30
po zmianie na systemowy status „Anulowane”30
po usunięciu pozycji zamówienia przez deleteOrderProduct31

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.

Ostatni wiersz tabeli zostaje jako wynik eksperymentu, nie jako zalecenie. Jeśli stany zasila ERP, każdy zapis stanu z drugiej strony jest ryzykiem rozjazdu, więc właściwą drogę anulowania trzeba najpierw sprawdzić w panelu BaseLinkera.
07

Kiedy zrobisz to sam, a kiedy nie

Sam

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”.

Z kimś, kto to robił

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.

FAQ: zamówienia z Magento w BaseLinkerze

W sprawdzonym wdrożeniu dlatego, że pozycje zamówienia nie były powiązane z produktami w katalogu BaseLinkera: pole product_id było puste, a w panelu kolumna „ID produktu” nie miała wartości. Bez powiązania BaseLinker nie ma czego zarezerwować ani zdjąć, choć zamówienie wygląda kompletnie.
Polem warehouse_id na każdej pozycji zamówienia, nie na zamówieniu. Dla produktu z katalogu BaseLinkera storage to „db”, a storage_id zero, tak jak w dokumentacji. Pominięte lub zerowe warehouse_id oznacza magazyn domyślny według źródła zamówienia, który może być innym magazynem, niż się spodziewasz.
W katalogu z włączonymi rezerwacjami towar jest rezerwowany po pobraniu zamówienia, a rezerwacja jest powiązana z pozycjami zamówienia. W sprawdzonej konfiguracji ani własny status anulowania, ani systemowy „Anulowane” nie przywróciły dostępności; przywróciło ją usunięcie pozycji. Następny krok to sprawdzenie w panelu osobnej operacji anulowania zamówienia i ustawień przetwarzania zamówień, które decydują o momencie realizacji; tego w teście nie mierzyliśmy. Usuwanie pozycji przez API traktuj jako wynik eksperymentu, nie zalecenie.
Identyfikator produktu z katalogu BaseLinkera zapisujemy w atrybucie produktu w Magento przy godzinnej synchronizacji stanów, bo odpowiedź API i tak go zawiera. Eksport zamówienia bierze go z atrybutu i wysyła na pozycji razem z warehouse_id, zamiast szukać produktu po EAN lub SKU od nowa.