Magazynowy system informacyjny a informatyczny

W literaturze przedmiotu magazynowy system informacyjny obejmuje całość: dane o towarze i zasady ich zapisu, a także pracowników oraz ich narzędzia. Magazynowy system informatyczny to jego część programowa i sprzętowa, czyli bazę danych z aplikacjami oraz urządzeniami. W ofertach i zapytaniach obie nazwy opisują zwykle ten sam produkt, więc dalej używamy ich zamiennie. Przedmiotem opisu jest to, co system musi przechowywać, aby stan magazynu dało się odtworzyć.

Jedna zasada porządkuje cały model: fizyczne przemieszczenie towaru zostaje w systemie jako dokument, a dokument zostaje jako ruch. Stan magazynowy jest skutkiem tych zapisów, a nie samodzielną liczbą, którą ktoś wpisuje. Dzięki temu każdą wartość da się prześledzić wstecz do konkretnej operacji i osoby.

PojęcieZakresPrzykładowe elementy
System informacyjnydane i zasady zapisu oraz ludzie z narzędziamiinstrukcja przyjęcia, uprawnienia magazyniera, kartoteka towarów
System informatycznyoprogramowanie oraz baza danych z sieciąserwer SQL Server, aplikacja webowa, terminale ze skanerem
Model danychstruktura tabel i relacji w bazietabele dokumentów oraz ruchów

Kolejne sekcje opisują ten model od strony dokumentów i transakcji. Warstwę techniczną (serwer i terminale) opisuje osobny tekst o informatycznym systemie magazynowym i jego warstwach, więc tutaj jej nie rozwijamy. Zarządzanie samym stanem (rezerwacje, stany minimalne, partie) opisuje strona o zarządzaniu stanami magazynowymi w systemie informatycznym.

Dokumenty magazynowe i ich skutek dla stanu

Dokument magazynowy jest jednostką księgową ruchu. Pięć typów pokrywa większość codziennych operacji, a ich skutek dla stanu pokazuje tabela poniżej. O rozróżnieniu decyduje to, czy towar przekracza granicę magazynu i czy zmienia jego wartość księgową. Pełny obieg dokumentów opisuje tekst o dokumentach magazynowych, a zestawienie tych skrótów omawia też artykuł na stronie SoftwareStudio.

DokumentZnaczenieSkutek dla stanuRuch w tabeli
PZprzyjęcie zewnętrzne od dostawcyzwiększa stan w lokalizacji przyjęciaruch dodatni
WZwydanie zewnętrzne do odbiorcyzmniejsza stan w lokalizacji wydaniaruch ujemny
MMprzesunięcie między magazynami lub lokalizacjamizmniejsza jedną lokalizację i zwiększa drugąpara ruchów o sumie zero
RWrozchód wewnętrzny, na przykład na produkcję albo próbkizmniejsza stan bez odbiorcy zewnętrznegoruch ujemny
PWprzychód wewnętrzny, na przykład z produkcjizwiększa stan bez dostawcyruch dodatni

Dwie cechy odróżniają MM od pozostałych. Dokument nie zmienia sumy zapasu w firmie, tylko jego rozmieszczenie, więc suma ilości po obu stronach musi wynosić zero. Zwykle wymaga też dwóch potwierdzeń: wydania z lokalizacji źródłowej i przyjęcia w docelowej. W czasie między nimi towar jest „w drodze”.

Reguła: zatwierdzony dokument nie podlega edycji, a błąd koryguje się dokumentem odwrotnym.

Cykl życia dokumentu

Dokument przechodzi przez statusy, z których tylko ostatni zmienia stan magazynowy. Poniższy podział jest typowy dla systemów magazynowych i służy jako punkt odniesienia; konkretne nazwy statusów zależą od produktu.

  • Bufor - dokument roboczy, który nie zmienia stanu i można go dowolnie edytować.
  • Zatwierdzony - treść jest zamrożona, a ilości mogą rezerwować towar do czasu realizacji.
  • Zaksięgowany - ruchy zostały zapisane w tabeli, a stan w lokalizacjach uległ zmianie.
  • Anulowany - dokument wycofany przed księgowaniem, bez ruchów w tabeli.
Ilustracja dokumentów magazynowych w systemie WMS - dokumenty przyjęć i wydań towaru
Dokumenty przyjęć i wydań jako źródło ruchów - tu kończy się ścieżka operacji, a zaczyna zapis w tabelach

Przy pracy na terminalu odczyt kodu tworzy pozycję dokumentu od razu, a nie po zakończeniu zmiany. Skanowanie w aplikacji, na przykład opisanej w tekście o aplikacji magazynowej na Androida, skraca więc czas między ruchem fizycznym a zapisem w bazie. Krótsze okno oznacza mniej sytuacji, w których stan systemowy odbiega od półki.

Numeracja dokumentów

Numer dokumentu, na przykład PZ/2026/000123, pochodzi z serii prowadzonej osobno dla każdego typu dokumentu i roku. Obiekt SEQUENCE w SQL Server nie gwarantuje ciągłości, bo wartość pobrana przez wycofaną transakcję przepada. Gdy przepisy lub audyt wymagają numeracji bez luk, używa się tabeli liczników zwiększanej w tej samej transakcji, w której zapisuje się dokument.

Model danych - dokument i ruch magazynowy

Minimalny model magazynowy składa się z pięciu grup tabel. Kartoteka towarowa opisuje, co jest przechowywane, a słownik lokalizacji - gdzie. Nagłówki i pozycje dokumentów zapisują, dlaczego coś się zmieniło, a tabela ruchów - o ile. Tabela stanów łączy wynik w bieżącą ilość per towar i lokalizacja.

TabelaKluczZawartośćZmienność
Kartoteka towaruTowarIdindeks, nazwa, jednostka miary, kod EANrzadko zmieniana
LokalizacjaLokalizacjaIdadres regału, strefa, typ miejscarzadko zmieniana
Dokument (nagłówek i pozycje)DokumentId, PozycjaNrtyp, kontrahent, status, ilościedytowalny do zatwierdzenia
RuchRuchIdilość ze znakiem, data, dokument, lokalizacjatylko dopisywanie
StanTowarId i LokalizacjaIdbieżąca ilośćzmieniany przy księgowaniu

Poniższy szkic w T-SQL pokazuje tabele ruchów i stanów. Nazwy tabel i kolumn są umowne i nie odzwierciedlają schematu żadnego konkretnego produktu.

CREATE TABLE dbo.Ruch (
    RuchId        BIGINT IDENTITY(1,1) PRIMARY KEY,
    DokumentId    INT           NOT NULL,
    PozycjaNr     SMALLINT      NOT NULL,
    TowarId       INT           NOT NULL,
    LokalizacjaId INT           NOT NULL,
    Ilosc         DECIMAL(18,3) NOT NULL,  -- dodatnia: przyjęcie, ujemna: wydanie
    DataRuchu     DATETIME2(0)  NOT NULL DEFAULT SYSDATETIME()
);

CREATE TABLE dbo.Stan (
    TowarId       INT           NOT NULL,
    LokalizacjaId INT           NOT NULL,
    Ilosc         DECIMAL(18,3) NOT NULL,
    CONSTRAINT PK_Stan PRIMARY KEY (TowarId, LokalizacjaId),
    CONSTRAINT CK_Stan_Ilosc CHECK (Ilosc >= 0)
);
  • Ilość ze znakiem - ruch dodatni zwiększa stan, a ujemny go zmniejsza, więc SUM(Ilosc) daje saldo bez rozróżniania typów dokumentów.
  • Ograniczenie CHECK - warunek nieujemnej ilości w tabeli stanów odrzuca wydanie ponad stan na poziomie bazy, niezależnie od aplikacji.
  • Tylko dopisywanie - tabela ruchów nie przyjmuje poleceń UPDATE ani DELETE, co można wymusić uprawnieniami DENY.
  • Klucz stanu - para TowarId i LokalizacjaId daje jeden wiersz na miejsce składowania i przyspiesza wyszukiwanie.

Partia i termin ważności w ruchu

Towar prowadzony partiami wymaga dodatkowej kolumny PartiaId w tabelach ruchów i stanów, a klucz stanu rośnie o tę kolumnę. Wtedy jedna kartoteka ma tyle wierszy stanu, ile partii leży w danej lokalizacji. Identyfikatory partii i dat trafiają do systemu z etykiety: w standardzie GS1 partię niesie identyfikator zastosowania (10), a termin ważności identyfikator (17), co opisuje strona o etykiecie logistycznej GS1.

Zasady rotacji FIFO i FEFO wynikają z tych samych kolumn. System wybiera do wydania partię o najwcześniejszej dacie, a szczegóły ewidencji opisuje tekst o partiach produkcyjnych i datach ważności.

Księgowanie ruchu w tabelach SQL Server

Księgowanie dokumentu jest jedną transakcją, która obejmuje zmianę tabeli stanów i dopisanie ruchu. Obie operacje muszą się udać razem albo wcale. W przeciwnym razie stan i suma ruchów rozjadą się bez śladu w aplikacji, a różnicę zauważy dopiero inwentaryzacja. Studio WMS.net działa na bazie MS SQL Server, więc opisane niżej mechanizmy dotyczą tego środowiska, a szerszy kontekst daje strona o systemie magazynowym opartym na SQL.

CREATE PROCEDURE dbo.KsiegujWydanie
    @DokumentId INT, @PozycjaNr SMALLINT, @TowarId INT,
    @LokalizacjaId INT, @Ilosc DECIMAL(18,3)
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;
    BEGIN TRANSACTION;

    UPDATE dbo.Stan WITH (UPDLOCK, ROWLOCK)
       SET Ilosc = Ilosc - @Ilosc
     WHERE TowarId = @TowarId
       AND LokalizacjaId = @LokalizacjaId
       AND Ilosc >= @Ilosc;

    IF @@ROWCOUNT = 0
        THROW 50001, N'Brak wystarczającego stanu w lokalizacji.', 1;

    INSERT dbo.Ruch (DokumentId, PozycjaNr, TowarId, LokalizacjaId, Ilosc)
    VALUES (@DokumentId, @PozycjaNr, @TowarId, @LokalizacjaId, -@Ilosc);

    COMMIT TRANSACTION;
END;

Warunek w klauzuli WHERE odrzuca aktualizację, gdy ilość w lokalizacji jest mniejsza od żądanej, a wskazówka UPDLOCK zakłada blokadę aktualizacyjną na czas transakcji. Drugi terminal, który próbuje wydać ten sam towar, czeka na zwolnienie blokady i po jej zwolnieniu widzi już nowy stan. Przy XACT_ABORT ON błąd rzucony przez THROW cofa całą transakcję.

Reguła: sprawdzenie ilości i jej zmniejszenie wykonuje jedna instrukcja UPDATE, a nie odczyt w aplikacji z późniejszym zapisem.

Poziom izolacji i blokady

Przy domyślnym poziomie READ COMMITTED odczyt nie widzi niezatwierdzonych zmian. Odczyt i późniejszy zapis tego samego wiersza nie tworzą jednak niepodzielnej operacji. Z tego powodu wzorzec „sprawdź, potem zapisz” w dwóch instrukcjach zawodzi przy dwóch równoległych wydaniach. Wybór poziomu izolacji zmienia także to, czy raport stanu czeka na księgowanie.

Poziom izolacjiZachowanie odczytuSkutek dla magazynu
READ COMMITTEDodczyt czeka na zwolnienie blokad zapisuraport stanu może czekać na trwające księgowanie
READ COMMITTED SNAPSHOTodczyt korzysta z wersji wierszaraport nie blokuje księgowania, rośnie użycie bazy tempdb
SNAPSHOTspójny obraz danych z początku transakcjikonflikt aktualizacji kończy się błędem 3960 i wymaga ponowienia
SERIALIZABLEblokady zakresów kluczynajsilniejsza ochrona i największe ryzyko blokad

Pełny opis poziomów i zachowania blokad zawiera dokumentacja Microsoft: SET TRANSACTION ISOLATION LEVEL oraz przewodnik po blokowaniu i wersjonowaniu wierszy.

Dwie transakcje, które blokują te same wiersze w odwrotnej kolejności, kończą się zakleszczeniem: silnik wybiera ofiarę i zwraca błąd 1205. Prosta praktyka ogranicza to ryzyko. Pozycje dokumentu księguje się zawsze w tej samej kolejności, na przykład rosnąco po TowarId, a transakcję ofiary ponawia się automatycznie.

Stan magazynowy jako suma ruchów

W praktyce spotyka się dwa sposoby obliczania stanu. Pierwszy liczy go za każdym razem z ruchów zapytaniem SUM, drugi trzyma wynik w osobnej tabeli i zmienia go przy księgowaniu. Różnica dotyczy kosztu odczytu i odporności na błędy.

  • Stan wyliczany - prosty i odporny na błędy, bo saldo zawsze wynika z historii, ale wolnieje wraz z liczbą ruchów.
  • Stan zapisany - odczytywany natychmiast po kluczu, lecz wymaga transakcji z poprzedniej sekcji i okresowej kontroli.

Rozwiązaniem pośrednim jest tabela stanów jako zapis zbiorczy i zapytanie kontrolne, które porównuje ją z sumą ruchów. Można je uruchamiać jako zadanie SQL Agent po zamknięciu dnia. Pełny outer join wychwytuje także towar, który ma ruchy, a nie ma wiersza stanu.

SELECT COALESCE(s.TowarId, r.TowarId)             AS TowarId,
       COALESCE(s.LokalizacjaId, r.LokalizacjaId) AS LokalizacjaId,
       s.Ilosc AS StanZapisany, r.SumaRuchow
FROM dbo.Stan AS s
FULL OUTER JOIN (
        SELECT TowarId, LokalizacjaId, SUM(Ilosc) AS SumaRuchow
        FROM dbo.Ruch
        GROUP BY TowarId, LokalizacjaId
     ) AS r
  ON r.TowarId = s.TowarId AND r.LokalizacjaId = s.LokalizacjaId
WHERE COALESCE(s.Ilosc, 0) <> COALESCE(r.SumaRuchow, 0);

Zapytania o ruchy jednego towaru przyspiesza indeks nieklastrowy po kolumnie TowarId z kolumnami dołączonymi. Taki indeks pokrywa zapytanie, więc silnik nie sięga po dane do tabeli. Składnię opisuje dokumentacja CREATE INDEX.

CREATE NONCLUSTERED INDEX IX_Ruch_Towar
    ON dbo.Ruch (TowarId)
    INCLUDE (LokalizacjaId, Ilosc, DataRuchu);

Sposób prezentowania i kontroli wartości w raportach opisuje strona o stanach magazynowych, a rezerwacje i stany dostępne omawia tekst o zarządzaniu stanami magazynowymi.

Dokumenty z ERP i zapis idempotentny

Większość dokumentów magazynowych nie powstaje w magazynie. Zlecenie przyjęcia lub wydania przychodzi z systemu ERP jako komunikat, a magazyn potwierdza jego wykonanie. Ten sam komunikat może dotrzeć dwa razy, na przykład po przerwie w łączności i ponowieniu wysyłki, a system nie może wtedy założyć drugiego dokumentu.

Przed duplikatem chroni numer zewnętrzny zapisany razem z nazwą systemu źródłowego. Unikalny indeks filtrowany odrzuca powtórzenie na poziomie bazy, a integracja traktuje taki błąd jako sygnał, że komunikat został już przetworzony. Nazwy w przykładzie są umowne.

CREATE UNIQUE NONCLUSTERED INDEX UX_Dokument_NrZewnetrzny
    ON dbo.Dokument (SystemZrodlowy, NrZewnetrzny)
    WHERE NrZewnetrzny IS NOT NULL;

Tryby wymiany danych z ERP, od plików po interfejsy usługowe, opisuje tekst o integracji systemów magazynowych. Po stronie magazynu liczy się jedno: ponowienie komunikatu nie może zmienić stanu drugi raz.

Storno i historia obrotów

Błąd w zatwierdzonym dokumencie koryguje się dokumentem odwrotnym, a nie edycją. Storno PZ tworzy ruchy o przeciwnym znaku i wskazuje dokument źródłowy w osobnej kolumnie. Historia obrotów zachowuje oba zapisy, więc audytor widzi zarówno pomyłkę, jak i jej naprawę.

Ilustracja historii obrotów magazynowych - lista ruchów towaru w kartotece w podziale na dokumenty
Historia obrotów towaru jako lista ruchów - podstawa do wyjaśniania rozbieżności po inwentaryzacji

Inwentaryzacja działa tym samym mechanizmem. Różnica między stanem spisanym a systemowym nie zmienia tabeli stanów wprost, tylko generuje dokument nadwyżek lub niedoborów, którego ruchy korygują saldo. Zasady liczenia i rozliczania opisuje tekst o inwentaryzacji w magazynie.

Archiwizacja i wielkość tabeli ruchów

Tabela ruchów rośnie liniowo z liczbą operacji, więc po kilku latach zapytania po dacie zaczynają obejmować miliony wierszy. Przy dużej historii tabelę można partycjonować po dacie ruchu, a starsze partycje przenosić do archiwum bez zmian w aplikacji. Saldo otwarcia okresu warto wtedy zapisywać jako osobny ruch, żeby suma po archiwizacji nadal zgadzała się ze stanem.

Ten sam podział obowiązków widać w operacjach dnia codziennego: kompletacja rezerwuje i zdejmuje towar, a przyjęcie go dopisuje, co szczegółowo opisuje tekst o operacjach magazynowych. Model danych pozostaje przy tym ten sam, niezależnie od tego, czy operację zainicjował człowiek, czy integracja z ERP.