Przejdź do głównej zawartości

Jak położyć zapas w binie

Cztery endpointy zmieniają zawartość binu. Wszystko inne w magazynie (przyjęcia, odkładanie, kompletacja, remanenty, przewozy, zwroty) sprowadza się do wywołania jednego z nich z innym powodem.

Wszystkie cztery leżą pod /api/_action/p2lab-stockly/warehouse/stock/, uwierzytelniają się jak każde wywołanie Admin API i są chronione tym samym uprawnieniem: p2lab_stockly.execute_stock_operations. Wszystkie cztery odpowiadają {"success": true}.

POST warehouse/stock/add

Towar przychodzi. Nie ma źródła; to jest wejście do budynku.

{
"warehouseId": "0191f1…",
"binLocationId": "0191f2…",
"productId": "0191f3…",
"quantity": 12,
"comment": "Przyjęte bez zamówienia zakupu",
"movementType": "add",
"batchNumber": "L-2411",
"expiresAt": "2027-03-31T00:00:00+00:00",
"handlingUnitCode": "LP-000241"
}
PoleWymaganeUwagi
warehouseIdtak
productIdtak
quantitytakliczba dodatnia; kierunek bierze się z endpointu, nigdy ze znaku
binLocationIdniepominięte — towar trafia na bin unassigned magazynu
commentniedowolny tekst, widoczny w historii
movementTypenieadd, return albo correction
batchNumber, expiresAtnierazem identyfikują partię, do której dołączają sztuki
handlingUnitCodenienośnik, na którym towar jest odkładany

POST warehouse/stock/remove

Towar wychodzi. Nie ma celu.

{
"warehouseId": "0191f1…",
"binLocationId": "0191f2…",
"productId": "0191f3…",
"quantity": 3,
"movementType": "damage",
"comment": "Zgniecione przy przeładunku",
"batchId": "0191f4…",
"cleanupEmptySource": false,
"handlingUnitId": "0191f5…",
"placementScoped": true
}
PoleWymaganeUwagi
warehouseId, productId, quantitytakjak wyżej
binLocationIdniepominięte znaczy bin unassigned, a nie „którykolwiek bin”
movementTypenieremove, damage, loss albo correction
batchIdniepobierz z tej partii, zamiast pozwolić wybrać strategii wydania
cleanupEmptySourcenieusuń wiersz, jeśli wydanie go opróżni
handlingUnitId + placementScopedniepobierz dokładnie z tego nośnika

Bez batchId sztuki są konsumowane partia po partii w kolejności narzuconej przez strategię wydania magazynu, a rejestr dostaje jeden wiersz na każdą ruszoną partię: wydanie obejmujące trzy partie to trzy ruchy, nie jeden.

placementScoped odróżnia „operator wskazał sztuki luzem” od „operator nic nie powiedział”. Z ustawionym handlingUnitId i placementScoped: true pobranie dotyka tego nośnika i żadnego innego; bez tego użyte zostanie pierwsze znalezione umiejscowienie, a to bywa niewłaściwe, gdy ta sama partia leży w binie i luzem, i na nośniku.

POST warehouse/stock/move

Towar jedzie. Oba końce istnieją, suma magazynu się nie zmienia.

{
"sourceWarehouseId": "0191f1…",
"sourceBinLocationId": "0191f2…",
"targetWarehouseId": "0191f1…",
"targetBinLocationId": "0191f6…",
"productId": "0191f3…",
"quantity": 6,
"movementType": "putaway",
"purchaseOrderItemId": "0191f7…",
"sourceHandlingUnitId": "0191f5…",
"sourcePlacementScoped": true,
"targetHandlingUnitCode": "LP-000242",
"cleanupEmptySource": true
}
PoleWymaganeUwagi
sourceWarehouseId, targetWarehouseId, productId, quantitytak
sourceBinLocationId, targetBinLocationIdniekażdy z nich można pominąć — to bin unassigned
movementTypeniemove, mapping, putaway albo putaway_undo
batchIdnieprzenieś konkretnie tę partię
operationIdniewiąże ruch z operacją magazynową
purchaseOrderId, purchaseOrderItemIdnieodkładanie towaru z przyjęcia
sourceHandlingUnitId + sourcePlacementScopedniez którego nośnika schodzą sztuki
targetHandlingUnitCodeniena jaki nośnik trafiają
cleanupEmptySourcenieusuń wiersz źródłowy, gdy się opróżni

Przeniesienie całej zawartości nośnika w obrębie jednego magazynu przemieszcza nośnik; przeniesienie części rozbija go, a reszta zostaje na miejscu. Podanie targetHandlingUnitCode to sposób, w jaki towar luzem trafia na paletę przy okazji przewozu.

Przewóz z podanym purchaseOrderId albo purchaseOrderItemId przelicza dodatkowo postęp odkładania tego przyjęcia. Zwykły ruch bin-do-binu nie niesie takiego odniesienia i nie zmienia niczego poza magazynem.

Źródło i cel nie mogą być tym samym miejscem, także w przypadku unassigned do unassigned, bo oba końce są rozwiązywane do binu przed sprawdzeniem.

POST warehouse/stock/map

Kładzie istniejący zapas nieprzypisany na prawdziwy bin w tym samym magazynie, zapisując to jako ruch typu mapping.

{
"warehouseId": "0191f1…",
"productId": "0191f3…",
"targetBinLocationId": "0191f6…",
"comment": "Mapowanie lokalizacji",
"batchId": "0191f4…"
}

To nie jest przewóz pod ładniejszą nazwą. Przewóz przemieszcza towar, który gdzieś był; mapowanie po raz pierwszy zapisuje, gdzie towar leżał cały czas, i dlatego korzysta z niego kreator wdrożenia. Gdy bin docelowy jest wolny, wiersz zachowuje tożsamość i po prostu dostaje bin; gdy produkt już ten bin zajmuje, oba wiersze się scalają.

Przeniesienie między magazynami to przewóz. Ten endpoint działa wyłącznie wewnątrz jednego magazynu.

Nieznany movementType degraduje się, nie kończy błędem. Każdy endpoint przyjmuje własną krótką listę i po cichu podstawia swoją wartość domyślną za wszystko inne: add dla wejścia, remove dla wyjścia, move dla trzeciego. Wywołanie się udaje, rejestr zapisuje niewłaściwy powód, a każdy raport grupujący po typie jest cicho nieprawdziwy. Nie ma tu błędu do złapania; wysyłaną wartość trzeba sprawdzić samodzielnie.

Ilość jest zawsze dodatnia. Wszystkie trzy endpointy odrzucają zero i wartości ujemne, zgłaszając P2LAB_STOCKLY__STOCK__INVALID_QUANTITY (HTTP 400). Kierunek siedzi w tym, który endpoint wywołałeś. Ujemny przewóz opróżniłby cel i powiększył źródło, czyli dokładnie odwrócił intencję.

Pominięcie binu to decyzja, nie luka. binLocationId: null za każdym razem znaczy bin unassigned: przy wejściu, przy wyjściu i na obu końcach przewozu. Nigdy nie znaczy „tam, gdzie ten produkt akurat leży”.

Bin transit jest zamknięty. Wskazanie go jako źródła albo celu kończy się odmową P2LAB_STOCKLY__STOCK__TRANSIT_SOURCE_NOT_ALLOWED / …_TARGET_NOT_ALLOWED (HTTP 409). Towar wchodzi tam, gdy przewóz wyrusza, i wychodzi, gdy przewóz dojedzie albo zostanie anulowany. Użyj endpointów przewozu.

Wydanie większe niż stan półki jest odrzucane, z błędem P2LAB_STOCKLY__STOCK__INSUFFICIENT_STOCK (HTTP 422), z podaniem, ile zażądano i ile było dostępne. Ilość nie jest po cichu przycinana do zera.

Zepsute żądanie to 400 z nazwą pola. Brakujące albo niepoprawne identyfikatory i nieliczbowe ilości kończą się P2LAB_STOCKLY__REQUEST__INVALID i komunikatem mówiącym, o które pole chodzi — zanim cokolwiek zostanie zapisane.

Magazyn może wymagać, by towar jeździł na nośniku. Tam, gdzie ta polityka jest włączona, dodanie bez handlingUnitCode jest odrzucane, podobnie jak przewóz kończący się towarem luzem po stronie celu — chyba że przewóz przenosi cały nośnik w obrębie tego samego magazynu, co samo w sobie spełnia wymóg. Reguła jest egzekwowana po stronie serwera, więc integracja pomijająca kod nie obchodzi jej po cichu.

Nośniki mają własne operacje strukturalne, wszystkie pod tym samym uprawnieniem i wszystkie neutralne dla stanu: towar zostaje w binie, zmienia się tylko to, na czym leży:

MetodaŚcieżkaCo robi
POSThandling-unit/palletizesztuki leżące w binie luzem trafiają na nośnik
POSThandling-unit/depalletizeodwrotnie
POSThandling-unit/{huId}/nestnośnik zostaje włożony w inny
POSThandling-unit/{huId}/unnesti z niego wyjęty
POSThandling-unit/{huId}/movecały nośnik jedzie do innego binu
GEThandling-unit/{huId}/contentsco na nim leży
GEThandling-unit/generate-codewolny kod do oznaczenia nowego nośnika

Wtyczka w tej samej instalacji może wołać StockService wprost. Metody odpowiadają endpointom (addStock(), removeStock(), moveStock(), mapStock()) i przyjmują własny Context wołającego.

Dwie rzeczy, które endpointy załatwiają same, a o których wołający wprost musi pomyśleć.

atomic() uruchamia kilka operacji jako jedną całość, na wypadek gdy połowicznie wykonana para byłaby gorsza niż błąd: postawienie werdyktu i przewiezienie towaru, zaksięgowanie linii remanentu i położenie jej na binie. Wyłącznie praca na bazie: zakleszczenie powtarza wywołanie od początku, więc wszystko, co ma skutek poza bazą, musi zostać na zewnątrz.

lockProductStockRows() jest pierwszą instrukcją każdej jednostki zapisu i endpointy wywołują ją same. To ona zamienia sprawdzenie dostępności w odczyt pod blokadą; bez niej dwa równoległe wydania widzą tę samą liczbę i rejestr zapisuje więcej wydanych sztuk, niż półka kiedykolwiek miała.

Po kolei, w jednej transakcji:

  1. wiersz ilości się zmienia, a razem z nim podział na partie;
  2. jeden wiersz rejestru na każdą ruszoną partię;
  3. za nimi idą umiejscowienia lotów i lustro nośników;
  4. product.stock zostaje przeliczony z półek;
  5. przy wydaniu, jeśli półka spadła poniżej tego, co przypisały zamówienia, przypisanie ustępuje, a różnica staje się zamówieniem oczekującym.

Fakty są publikowane po zatwierdzeniu transakcji: p2lab_stockly.stock.moved dla samego ruchu i p2lab_stockly.stock.availability_changed, gdy przeliczenie faktycznie zmieniło to, co da się sprzedać. Oba opisuje katalog zdarzeń.

Przed tym wszystkim wysyłane jest BeforeStockOperationEvent, poza transakcją, i nasłuchujący może odmówić wykonania całej operacji. Zobacz punkty decyzyjne.