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).
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)).
sum(QuantityOnHand) over (partition by ProductID
order by case when IsPerishable = 1 then ExpiryDate else ReceivedDate end, BatchID
rows unbounded preceding) as SupplyEndKaż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] |
Analogicznie pozycje zamówienia (tu jedna) dostają przedziały na tej samej osi:
| Pozycja | QuantityOrdered | Przedział popytu |
|---|---|---|
| #1 | 150 | (0, 150] |
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.
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.
W tej samej transakcji procedura wstawia:
BatchAllocations— po wierszu na każde przecięcie (pozycja × partia) zUnitCostAtAllocationzamrożonym na koszcie partii (stąd raport marży rzeczywistej),StockMovements— wpisySALEper partia; triggertr_UpdateQuantityOnHandaktualizuje z nich cacheQuantityOnHand.
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.