Przejdź do głównej zawartości

Zamówienie zakupu z systemu dostawcy

Dla: Programista

Drukarnia zamawia papier, farby i folie w systemie dostawcy: na portalu B2B hurtowni, przez EDI albo we własnym systemie zakupów. Ten przepis przenosi to do Prynt:

  • zamówienie z systemu dostawcy zapisuje w Prynt jako zamówienie zakupu, tylko raz, i oznacza je jako wysłane do dostawcy;
  • dostawę z dokumentu WZ przyjmuje na magazyn: stan materiałów rośnie, a każde przyjęcie jest ruchem magazynowym powiązanym z zamówieniem, z numerem i datą WZ oraz ceną pozycji. Tej samej dostawy nie przyjmie drugi raz.
  • Klucz z zakresami warehouse:read i warehouse:write. Ceny pozycji zamówienia zapisujesz bez prices:read; zakres jest potrzebny dopiero do ich odczytu.
  • Klienta API z przepisu Obsługa błędów i ponowień. Kod niżej wysyła żądania przez niego.
  • Karty materiałów w magazynie Prynt. Kod szuka papieru, farby albo folii po kodzie z magazynu Prynt (code, np. PA-12). Karty zakłada i prowadzi drukarnia: integracja nie dodaje ani nie zmienia materiałów. Jeśli dostawca ma własne kody towarów, trzymaj u siebie tabelę: kod dostawcy → kod w Prynt.

Numer zamówienia z systemu dostawcy zapisujesz w polu externalRef zamówienia zakupu. Filtr externalRef listy purchaseOrders znajduje po nim zamówienie, więc nie musisz przechowywać identyfikatorów z Prynt.

  • Należy do Twojej integracji. Inna integracja ma własne identyfikatory i nie widzi Twoich, a pracownicy drukarni widzą identyfikator tylko w historii zmian zamówienia.
  • Jeden na zamówienie, unikalny w integracji. Identyfikator, który ma już inne zamówienie, API odrzuca błędem CONFLICT. null w updatePurchaseOrder usuwa identyfikator.
  1. Szukaj zamówienia po externalRef. Znalezione znaczy, że zostało już zapisane: nie twórz drugiego. Szkic (DRAFT) kod tylko oznacza jako wysłany.

  2. Znajdź dostawcę po NIP, wysyłając same cyfry. Filtr nip szuka fragmentu numeru, więc porównaj cały NIP u siebie. Nieznanego dostawcę dodaj mutacją createSupplier z nagłówkiem Idempotency-Key z NIP: ponowienie nie doda drugiego. Dostawcę, którego drukarnia wycofała (isActive: false), kod zostawia jej decyzji: nie zakłada go drugi raz i zgłasza błąd — zamówienia u nieaktywnego dostawcy API i tak nie przyjmie.

  3. Znajdź materiały pozycji po kodzie. Filtr code też szuka fragmentu (PA-1 znajdzie też PA-10…PA-199), więc kod sortuje wyniki po kodzie (CODE, rosnąco), porównuje cały kod i przechodzi na kolejną stronę, dopóki go nie znajdzie albo strony się nie skończą. Materiału, którego nie ma w magazynie, kod nie zamawia: zgłasza błąd i nie tworzy zamówienia.

  4. Utwórz zamówienie mutacją createPurchaseOrder z numerem z systemu dostawcy w externalRef i nagłówkiem Idempotency-Key z tego numeru. Ponowienie nie utworzy drugiego zamówienia, a po 24 godzinach klucza chroni przed tym wyszukiwanie z kroku 1.

  5. Oznacz zamówienie jako wysłane mutacją updatePurchaseOrderStatus ze statusem SENT: zamówienie złożone w systemie dostawcy jest już u dostawcy. Ten sam status wysłany ponownie niczego nie zmienia.

Pozycja wskazuje materiał (materialType i materialId), którego stan podniesie dostawa, i podaje ilość w jednostce zakupu. Jednostka musi dać się przeliczyć na jednostkę stanu materiału:

Materiał Jednostki pozycji (np.) Na stanie
Papier ark., arkusz, szt, sheet arkusze 1:1
Farba g, kg gramy; 1 kg to 1000 g
Folia rolki, rolka, roll albo m, mb rolki; metry przez metersPerRoll

Pełną listę jednostek każdego materiału podaje opis pola unit w referencji API. Domyślna jednostka szt pasuje tylko do papieru: pozycji farby i folii zawsze podawaj jednostkę. Inną jednostkę, np. palety, API odrzuca błędem STOCK_UNIT_NOT_CONVERTIBLE z jednostką stanu w params, tak samo metry folii, która nie ma ustawionych metrów na rolce. Pozycja bez materiału (materialId: null) to towar spoza magazynu: jej dostawa nie zmienia stanu.

Status zamówienia ustawiasz tylko mutacją updatePurchaseOrderStatus: SENT (ze szkicu albo z CONFIRMED), CONFIRMED (tylko z SENT, gdy dostawca potwierdził) albo CANCELLED (anulowanie przed pełnym przyjęciem). PARTIALLY_RECEIVED i RECEIVED wynikają z dostaw. Zmianę terminu dostawy, uwag albo pozycji wysyłasz łatką updatePurchaseOrder: pole pominięte zostaje bez zmian, a items zastępuje całą listę pozycji. Pozycji, z której coś już przyjęto, nie usuniesz, nie zmniejszysz poniżej przyjętej ilości i nie zmienisz jej materiału ani jednostki. Pozycja bez id dochodzi przy każdym wywołaniu (kopia z poprzedniego znika), więc zmianę z nowymi pozycjami ponawiaj z nagłówkiem Idempotency-Key. Zamówień zakupu API nie usuwa.

  1. Znajdź zamówienie po externalRef, czyli po numerze z systemu dostawcy.

  2. Sprawdź, czy ta dostawa nie jest już przyjęta. Każde przyjęcie to ruch magazynowy z numerem WZ, który podał kod (documentNumber). Jedno zapytanie stockMovements z filtrem { purchaseOrderId, documentNumber } i pageSize: 1 odpowiada na pytanie: totalCount większy od zera znaczy, że dostawa już jest na stanie. Filtr documentNumber porównuje cały numer bez rozróżniania wielkości liter (wz 412/2026 znajdzie WZ 412/2026, ale WZ 412 już nie). To chroni przed podwójnym przyjęciem także po 24 godzinach klucza idempotencji.

  3. Przyjmij dostawę mutacją receivePurchaseOrder: dla każdej pozycji WZ pozycja zamówienia (itemId) i ilość, która przyszła, w jednostce pozycji (np. kg), a do tego numer WZ (documentNumber) i data dostawy z dokumentu (deliveredAt). Nagłówek Idempotency-Key składa się z numeru zamówienia i numeru WZ: jedna WZ to jeden zapis.

Co robi przyjęcie:

  • Stan rośnie w jednostce stanu. 5 kg farby to 5000 g na stanie, 300 m folii przy 150 m na rolce to 2 rolki. Każda pozycja z materiałem zapisuje ruch RECEIPT z purchaseOrderId i przesuwa updatedAt materiału, więc synchronizacja z przepisu Stany magazynowe do BI widzi dostawę w następnej porcji.
  • Ruch zapisuje dokument dostawy. Każdy ruch dostawy ma cenę i jednostkę pozycji (unitPrice, priceUnit — ark., kg, rolki, m), walutę i dostawcę zamówienia (currency, supplierId), numer i datę WZ (documentNumber, deliveredAt) oraz wartość netto netValue: ilość z dokumentu razy cena, niezależnie od zaokrąglenia stanu. Cenę i wartość odczytasz tylko z zakresem prices:read. To migawka: późniejsza zmiana ceny pozycji nie zmienia ruchu, a cena, waluta i dostawca na karcie materiału zostają bez zmian.
  • Nie więcej, niż zamówiono. Suma przyjęć pozycji nie może przekroczyć zamówionej ilości, także gdy dwie dostawy przychodzą jednocześnie: nadwyżkę API odrzuca błędem PURCHASE_ORDER_OVER_RECEIVED, a w params podaje pozycję (itemId), ilość zamówioną (ordered), przyjętą wcześniej (alreadyReceived) i z tej dostawy (requested) w jej jednostce (unit). Nadwyżkę uzgodnij z drukarnią: zmień ilość pozycji (updatePurchaseOrder) albo przyjmij ją na kartę materiału mutacją receivePaper, receivePaint albo receiveFoil z tym samym documentNumber, deliveredAt, dostawcą (supplierId) i ceną (unitPrice za priceUnit). Takie przyjęcie nie ma purchaseOrderId, a każdy zapis wysyłaj z własnym Idempotency-Key (np. UUID zapisu, nie numer WZ: jedna WZ bywa kilkoma liniami tego samego materiału).
  • Papieru tylko do wyceny klucz nie przyjmie. Dostawę pozycji z papierem isStock: false (drukarnia go wycenia, ale nie prowadzi jego stanu) API odrzuca błędem PAPER_PRICING_ONLY i nie przyjmuje żadnej pozycji tej dostawy, tak samo jak receivePaper i withdrawPaper takiego papieru. Papier, który zablokował dostawę, podają params (paperId, code, name). Jeśli drukarnia jednak go magazynuje, poproś ją o zmianę karty papieru w Prynt.
  • Status wynika z dostaw. Część pozycji przyjęta: PARTIALLY_RECEIVED; wszystko: RECEIVED z datą receivedAt. Dostawę przyjmiesz tylko do zamówienia SENT, CONFIRMED albo PARTIALLY_RECEIVED; inny status (np. szkic albo zamówienie anulowane) API odrzuca błędem PURCHASE_ORDER_NOT_RECEIVABLE ze statusem w params.
  • Precyzja stanu. Stan folii jest liczony z dokładnością do 0,01 rolki: dostawa kilku metrów długiej rolki podnosi przyjętą ilość pozycji, ale nie zmienia stanu i nie zapisuje ruchu. Nie ma więc ruchu z jej numerem WZ, ceną ani wartością: cena zostaje tylko w pozycji zamówienia (PurchaseOrder.items.unitPrice), a w sumie netValue przyjęć tej dostawy nie ma. Takiej dostawy kod nie rozpozna potem po numerze WZ wśród ruchów — jeśli to możliwe w Twoim systemie, zapamiętaj u siebie numery przyjętych WZ.

Każdy wariant przyjmuje zamówienie i dostawę w takim kształcie (dopasuj go do systemu dostawcy):

zakup.json
{
"number": "ZZ/2026/0042",
"supplier": {
"name": "Papyrus sp. z o.o.",
"nip": "526-000-12-34",
"email": "[email protected]"
},
"currency": "PLN",
"expectedDelivery": "2026-10-20",
"lines": [
{
"code": "PA-12",
"kind": "PAPER",
"description": "Kreda mat 130g 70x100",
"quantity": 2000,
"unit": "ark.",
"netPrice": 0.41
},
{
"code": "F-1208",
"kind": "PAINT",
"description": "Pantone 1935 C",
"quantity": 5,
"unit": "kg",
"netPrice": 175
}
]
}
wz.json
{
"id": "WZ 412/2026",
"date": "2026-10-24",
"lines": [{ "code": "PA-12", "kind": "PAPER", "quantity": 1500 }]
}

kind to rodzaj materiału w Prynt (PAPER, PAINT albo FOIL), code jego kod w magazynie Prynt, a netPrice cena netto jednostki zakupu w walucie zamówienia. id dokumentu WZ to jego numer (documentNumber), a date — data dostawy (deliveredAt, może jej nie być). Wysłanie zwraca id, code i status zamówienia w Prynt oraz created (czy powstało teraz); przyjęcie — status po dostawie i received (false, gdy ta dostawa była już przyjęta).

Przykłady pokazują adres zastępczy. Wpisz adres swojej drukarni, żeby go podmienić.

Wymaga curl, jq i skryptu prynt-call.sh z przepisu Obsługa błędów i ponowień w tym samym katalogu.

purchase-order.sh
#!/usr/bin/env bash
# purchase-order.sh — zamówienia zakupu z systemu dostawcy w Prynt (curl, jq; prynt-call.sh z
# przepisu „Obsługa błędów i ponowień”).
# ./purchase-order.sh send zakup.json zamówienie zakupu w Prynt (raz) i wysłane (SENT)
# ./purchase-order.sh receive ZZ/2026/0042 wz.json dostawa (dokument WZ) przyjęta do magazynu (raz)
set -euo pipefail
find_purchase_order='query FindPurchaseOrder($ref: String!) {
purchaseOrders(filter: { externalRef: $ref }) {
items { id code status items { id materialId } }
}
}'
find_supplier='query FindSupplier($nip: String!) {
suppliers(filter: { nip: $nip }) { items { id nip isActive } }
}'
create_supplier='mutation CreateSupplier($input: CreateSupplierInput!) {
createSupplier(input: $input) { id }
}'
# Filtr `code` szuka fragmentu kodu: kody rosnąco, strona po stronie, do kodu całego.
find_material='query FindMaterial($code: String!, $page: Int!) {
papers(
filter: { code: $code }
sort: { field: CODE, direction: ASC }
pagination: { page: $page, pageSize: 100 }
) { items { id code } pageInfo { hasNextPage } }
paints(
filter: { code: $code }
sort: { field: CODE, direction: ASC }
pagination: { page: $page, pageSize: 100 }
) { items { id code } pageInfo { hasNextPage } }
foils(
filter: { code: $code }
sort: { field: CODE, direction: ASC }
pagination: { page: $page, pageSize: 100 }
) { items { id code } pageInfo { hasNextPage } }
}'
create_purchase_order='mutation CreatePurchaseOrder($input: CreatePurchaseOrderInput!) {
createPurchaseOrder(input: $input) { id code status items { id materialId } }
}'
send_purchase_order='mutation SendPurchaseOrder($id: ID!) {
updatePurchaseOrderStatus(input: { id: $id, status: SENT }) { id code status }
}'
# Ruchy zamówienia z tym numerem dokumentu dostawy (dokładnie ten numer, bez wielkości liter).
delivery_receipts='query DeliveryReceipts($id: ID!, $documentNumber: String!) {
stockMovements(
filter: { purchaseOrderId: $id, documentNumber: $documentNumber }
pagination: { page: 1, pageSize: 1 }
) { totalCount }
}'
receive_delivery='mutation ReceiveDelivery($id: ID!, $input: ReceivePurchaseOrderInput!) {
receivePurchaseOrder(id: $id, input: $input) { id code status }
}'
body=$(mktemp) result=$(mktemp)
trap 'rm -f "$body" "$result"' EXIT
# Wysyła operację $1 ze zmiennymi $2 (JSON); odpowiedź (`data`) zostaje w pliku $result.
request() {
jq -n --arg query "$1" --argjson variables "$2" '{ query: $query, variables: $variables }' >"$body"
./prynt-call.sh "$body" | jq .data >"$result"
}
# Zamówienie zakupu w Prynt o numerze $1 w systemie dostawcy (externalRef) albo pusty wynik.
find_order() {
request "$find_purchase_order" "$(jq -n --arg ref "$1" '{ ref: $ref }')"
jq -c '.purchaseOrders.items[0] // empty' "$result"
}
# Dostawca po całym NIP z pliku zamówienia $1; nieznanego dodajemy — raz: klucz idempotencji
# z NIP chroni przed zdublowaniem, gdy odpowiedź na pierwszą próbę zginęła. Dostawcy, którego
# drukarnia wycofała (isActive: false), nie zakładamy drugi raz i nic u niego nie zamawiamy: Prynt
# i tak odrzuci takie zamówienie, a przywrócić go może tylko drukarnia.
ensure_supplier() {
local nip found
nip=$(jq -r '.supplier.nip | gsub("[^0-9]"; "")' "$1")
request "$find_supplier" "$(jq -n --arg nip "$nip" '{ nip: $nip }')"
# Filtr szuka fragmentu numeru: bierzemy tylko dostawcę z całym tym samym NIP.
found=$(jq -r --arg nip "$nip" '[.suppliers.items[] | select(.nip == $nip and .isActive)][0].id // empty' "$result")
if [[ -n $found ]]; then echo "$found"; return; fi
if jq -e --arg nip "$nip" 'any(.suppliers.items[]; .nip == $nip)' "$result" >/dev/null; then
echo "Dostawca z NIP $nip jest w Prynt nieaktywny: przywrócić go może drukarnia" >&2
return 1
fi
IDEMPOTENCY_KEY="dostawca-$nip" request "$create_supplier" \
"$(jq --arg nip "$nip" '{ input: { name: .supplier.name, nip: $nip, email: (.supplier.email // null) } }' "$1")"
jq -r '.createSupplier.id' "$result"
}
# Id materiału rodzaju $1 o kodzie $2 w magazynie Prynt (karty zakłada drukarnia). Filtr szuka
# fragmentu kodu („PA-1” pasuje też do „PA-10”…„PA-199”), więc idziemy po stronach do materiału
# z całym tym samym kodem.
material_id() {
local id list page=1
list=$(jq -rn --arg kind "$1" '{ PAPER: "papers", PAINT: "paints", FOIL: "foils" }[$kind]')
while true; do
request "$find_material" "$(jq -n --arg code "$2" --argjson page "$page" '{ code: $code, page: $page }')"
id=$(jq -r --arg list "$list" --arg code "$2" '[.[$list].items[] | select(.code == $code)][0].id // empty' "$result")
if [[ -n $id ]]; then echo "$id"; return; fi
if ! jq -e --arg list "$list" '.[$list].pageInfo.hasNextPage' "$result" >/dev/null; then
echo "Materiał $2 ($1) nie istnieje w magazynie Prynt" >&2
return 1
fi
page=$((page + 1))
done
}
# Czy dostawa o numerze WZ $2 jest już przyjęta: ruch zamówienia $1 z tym numerem dokumentu.
delivery_received() {
request "$delivery_receipts" "$(jq -n --arg id "$1" --arg wz "$2" '{ id: $id, documentNumber: $wz }')"
jq -e '.stockMovements.totalCount > 0' "$result" >/dev/null
}
# Wysyła zamówienie zakupu z pliku $1 do Prynt — raz: najpierw szuka go po numerze (externalRef),
# a nowe tworzy z kluczem idempotencji. Zamówienie przekazane dostawcy oznacza jako wysłane (SENT);
# ponowienie niczego nie zmienia.
send() {
local number found created=false supplier ids='[]' id i
number=$(jq -r .number "$1")
found=$(find_order "$number")
if [[ -z $found ]]; then
created=true
supplier=$(ensure_supplier "$1")
for ((i = 0; i < $(jq '.lines | length' "$1"); i++)); do
id=$(material_id "$(jq -r ".lines[$i].kind" "$1")" "$(jq -r ".lines[$i].code" "$1")")
ids=$(jq -c --arg id "$id" '. + [$id]' <<<"$ids")
done
# Pozycje wskazują materiały, których stan podniesie dostawa.
IDEMPOTENCY_KEY="zamowienie-zakupu-$number" request "$create_purchase_order" \
"$(jq --arg supplier "$supplier" --argjson ids "$ids" '{ input: {
supplierId: $supplier,
currency: .currency,
expectedDeliveryAt: (if .expectedDelivery then "\(.expectedDelivery)T00:00:00.000Z" else null end),
notes: "Zamówienie \(.number) z systemu dostawcy",
externalRef: .number,
items: [.lines | to_entries[] | {
materialType: .value.kind,
materialId: $ids[.key],
description: .value.description,
quantity: .value.quantity,
unit: .value.unit,
unitPrice: .value.netPrice
}]
} }' "$1")"
found=$(jq -c .createPurchaseOrder "$result")
fi
if [[ $(jq -r .status <<<"$found") == DRAFT ]]; then
request "$send_purchase_order" "$(jq '{ id }' <<<"$found")"
found=$(jq -c --slurpfile sent "$result" '. + $sent[0].updatePurchaseOrderStatus' <<<"$found")
fi
jq -c --argjson created "$created" '{ id, code, status, created: $created }' <<<"$found"
}
# Przyjmuje dostawę (dokument WZ) z pliku $2 do zamówienia zakupu o numerze $1: numer WZ i datę
# dostawy zapisuje w ruchach magazynowych (documentNumber, deliveredAt). Dostawę przyjętą wcześniej
# rozpoznaje po tym numerze wśród ruchów zamówienia — także po 24 h klucza idempotencji — i drugi
# raz jej nie przyjmuje.
receive() {
local number=$1 delivery=$2 order wz items='[]' i material item
order=$(find_order "$number")
if [[ -z $order ]]; then
echo "Zamówienie zakupu $number nie istnieje w Prynt" >&2
return 1
fi
wz=$(jq -r .id "$delivery")
if delivery_received "$(jq -r .id <<<"$order")" "$wz"; then
jq -c '{ id, status, received: false }' <<<"$order"
return
fi
for ((i = 0; i < $(jq '.lines | length' "$delivery"); i++)); do
material=$(material_id "$(jq -r ".lines[$i].kind" "$delivery")" "$(jq -r ".lines[$i].code" "$delivery")")
item=$(jq -r --arg material "$material" '[.items[] | select(.materialId == $material)][0].id // empty' <<<"$order")
if [[ -z $item ]]; then
echo "Pozycja $(jq -r ".lines[$i].code" "$delivery") nie należy do zamówienia $number" >&2
return 1
fi
items=$(jq -c --arg item "$item" --argjson quantity "$(jq ".lines[$i].quantity" "$delivery")" \
'. + [{ itemId: $item, receivedQuantity: $quantity }]' <<<"$items")
done
IDEMPOTENCY_KEY="dostawa-$number-$wz" request "$receive_delivery" \
"$(jq --argjson order "$order" --argjson items "$items" '{ id: $order.id, input: {
items: $items,
documentNumber: .id,
deliveredAt: (if .date then "\(.date)T00:00:00.000Z" else null end)
} }' "$delivery")"
jq -c --argjson order "$order" '{ id: $order.id, status: .receivePurchaseOrder.status, received: true }' "$result"
}
case ${1:-} in
send) send "$2" ;;
receive) receive "$2" "$3" ;;
*) echo "Użycie: $0 send <zamówienie.json> | receive <numer zamówienia> <dostawa.json>" >&2; exit 2 ;;
esac
Okno terminala
./purchase-order.sh send zakup.json # zamówienie zakupu w Prynt, raz
./purchase-order.sh receive ZZ/2026/0042 wz.json # dostawa na stan, raz
  • Potwierdzenie od dostawcy zapiszesz statusem CONFIRMED, a nowy termin dostawy łatką updatePurchaseOrder z expectedDeliveryAt.
  • Wydania na produkcję zapisujesz mutacjami withdrawPaper i withdrawPaint z ilością wydawaną (arkusze, gramy) oraz withdrawFoil z liczbą arkuszy i długością arkusza w milimetrach (numberOfSheets, sheetLengthMm), którą Prynt przelicza na rolki przez metersPerRoll. API nie pozwoli zejść poniżej zera i odrzuci wydanie ponad stan błędem STOCK_INSUFFICIENT, a wydanie papieru tylko do wyceny (isStock: false) błędem PAPER_PRICING_ONLY.
  • Stany i ruchy do raportów: przepis Stany magazynowe do BI.