Skip to content

Latest commit

 

History

History
89 lines (63 loc) · 3.61 KB

File metadata and controls

89 lines (63 loc) · 3.61 KB

Algorytm alokacji partii (sp_AllocateOrder)

Alokacja odpowiada na pytanie: z których partii i po ile sztuk wydać towar do zamówienia? Zamiast pętli po pozycjach i partiach, sp_AllocateOrder liczy całe zamówienie jednym zapytaniem metodą przedziałów narastających (interval matching).

Krok 0 — dane wejściowe

Zamówienie #501 na 150 szt. mleka (produkt perishable → sortowanie FEFO, po ExpiryDate). Na stanie trzy partie:

Partia ExpiryDate QuantityOnHand UnitCost
A 2026-07-20 100 3,10 zł
B 2026-08-05 80 3,40 zł
C 2026-09-01 200 2,95 zł

Partie przeterminowane i z zerowym stanem odpadają już w WHERE (QuantityOnHand > 0 and (ExpiryDate is null or ExpiryDate > dzisiaj)).

Krok 1 — podaż: suma narastająca po partiach w kolejności FEFO

sum(QuantityOnHand) over (partition by ProductID
    order by case when IsPerishable = 1 then ExpiryDate else ReceivedDate end, BatchID
    rows unbounded preceding) as SupplyEnd

Każda partia dostaje przedział „od–do" na wspólnej osi sztuk:

Partia QuantityOnHand Przedział podaży
A 100 (0, 100]
B 80 (100, 180]
C 200 (180, 380]

Krok 2 — popyt: suma narastająca po pozycjach zamówienia

Analogicznie pozycje zamówienia (tu jedna) dostają przedziały na tej samej osi:

Pozycja QuantityOrdered Przedział popytu
#1 150 (0, 150]

Krok 3 — alokacja = część wspólna przedziałów

Pozycja bierze z partii dokładnie tyle, ile wynosi przecięcie ich przedziałów:

QuantityTaken = min(DemandEnd, SupplyEnd) - max(DemandStart, SupplyStart)
Pozycja × partia Przecięcie QuantityTaken
#1 × A (0, 100] ∩ (0, 150] 100 szt.
#1 × B (100, 150] ∩ (100, 180] 50 szt.
#1 × C (150, 150] ∩ (180, 380] — (puste)

Wynik: 100 szt. z partii A + 50 szt. z partii B — podział na partie wychodzi z algebry przedziałów, bez żadnej pętli. Gdy zamówienie ma kilka pozycji tego samego produktu, suma narastająca po pozycjach układa je na osi jedna za drugą i mechanizm działa tak samo.

Krok 4 — brak towaru = rollback całości

Po alokacji procedura porównuje per produkt „zamówiono" z „przydzielono". Gdyby zamówienie opiewało na 400 szt. (podaż = 380), przecięcia pokryłyby tylko 380 szt. — procedura robi wtedy ROLLBACK i rzuca błąd 50005:

Brak towaru dla zamowienia 501: Mleko UHT Classic (brakuje 20 szt.)

Nie ma realizacji częściowych: albo całe zamówienie, albo nic.

Krok 5 — zapis i spójność

W tej samej transakcji procedura wstawia:

  • BatchAllocations — po wierszu na każde przecięcie (pozycja × partia) z UnitCostAtAllocation zamrożonym na koszcie partii (stąd raport marży rzeczywistej),
  • StockMovements — wpisy SALE per partia; trigger tr_UpdateQuantityOnHand aktualizuje z nich cache QuantityOnHand.

Współbieżność

Partie czytane są z with (updlock, holdlock) — blokada pesymistyczna trzymana do końca transakcji. Druga sesja walcząca o te same partie czeka, a po zwolnieniu blokad widzi już pomniejszone stany i przy braku towaru dostaje czytelny błąd zamiast ujemnego stanu. Dowód: sql/05_concurrency_test.sql.