Zamówienie zakupu z systemu dostawcy
Dla: Programista
Przykłady na tej stronie używają adresu . Zmień adres
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.
Czego potrzebujesz
Dział zatytułowany „Czego potrzebujesz”- Klucz z zakresami
warehouse:readiwarehouse:write. Ceny pozycji zamówienia zapisujesz bezprices: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.
Własny identyfikator zamówienia: externalRef
Dział zatytułowany „Własny identyfikator zamówienia: externalRef”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.nullwupdatePurchaseOrderusuwa identyfikator.
Zamówienie zakupu w Prynt
Dział zatytułowany „Zamówienie zakupu w Prynt”-
Szukaj zamówienia po
externalRef. Znalezione znaczy, że zostało już zapisane: nie twórz drugiego. Szkic (DRAFT) kod tylko oznacza jako wysłany. -
Znajdź dostawcę po NIP, wysyłając same cyfry. Filtr
nipszuka fragmentu numeru, więc porównaj cały NIP u siebie. Nieznanego dostawcę dodaj mutacjącreateSupplierz nagłówkiemIdempotency-Keyz 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. -
Znajdź materiały pozycji po kodzie. Filtr
codeteż szuka fragmentu (PA-1znajdzie 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. -
Utwórz zamówienie mutacją
createPurchaseOrderz numerem z systemu dostawcy wexternalRefi nagłówkiemIdempotency-Keyz tego numeru. Ponowienie nie utworzy drugiego zamówienia, a po 24 godzinach klucza chroni przed tym wyszukiwanie z kroku 1. -
Oznacz zamówienie jako wysłane mutacją
updatePurchaseOrderStatusze statusemSENT: 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.
Dostawa: przyjęcie dokumentu WZ
Dział zatytułowany „Dostawa: przyjęcie dokumentu WZ”-
Znajdź zamówienie po
externalRef, czyli po numerze z systemu dostawcy. -
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 zapytaniestockMovementsz filtrem{ purchaseOrderId, documentNumber }ipageSize: 1odpowiada na pytanie:totalCountwiększy od zera znaczy, że dostawa już jest na stanie. FiltrdocumentNumberporównuje cały numer bez rozróżniania wielkości liter (wz 412/2026znajdzieWZ 412/2026, aleWZ 412już nie). To chroni przed podwójnym przyjęciem także po 24 godzinach klucza idempotencji. -
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łówekIdempotency-Keyskł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
RECEIPTzpurchaseOrderIdi przesuwaupdatedAtmateriał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ść nettonetValue: ilość z dokumentu razy cena, niezależnie od zaokrąglenia stanu. Cenę i wartość odczytasz tylko z zakresemprices: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 wparamspodaje 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,receivePaintalboreceiveFoilz tym samymdocumentNumber,deliveredAt, dostawcą (supplierId) i ceną (unitPricezapriceUnit). Takie przyjęcie nie mapurchaseOrderId, a każdy zapis wysyłaj z własnymIdempotency-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łędemPAPER_PRICING_ONLYi nie przyjmuje żadnej pozycji tej dostawy, tak samo jakreceivePaperiwithdrawPapertakiego 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:RECEIVEDz datąreceivedAt. Dostawę przyjmiesz tylko do zamówieniaSENT,CONFIRMEDalboPARTIALLY_RECEIVED; inny status (np. szkic albo zamówienie anulowane) API odrzuca błędemPURCHASE_ORDER_NOT_RECEIVABLEze statusem wparams. - 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 sumienetValueprzyjęć 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):
{ "number": "ZZ/2026/0042", "supplier": { "name": "Papyrus sp. z o.o.", "nip": "526-000-12-34", }, "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 } ]}{ "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.
#!/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./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// purchase-order.mjs — Node.js 18+, klient API z przepisu „Obsługa błędów i ponowień”import { pryntRequest } from './prynt-client.mjs';
const FIND_PURCHASE_ORDER = `query FindPurchaseOrder($ref: String!) { purchaseOrders(filter: { externalRef: $ref }) { items { id code status items { id materialId } } }}`;
const FIND_SUPPLIER = `query FindSupplier($nip: String!) { suppliers(filter: { nip: $nip }) { items { id nip isActive } }}`;
const 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.const 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 } }}`;
const CREATE_PURCHASE_ORDER = `mutation CreatePurchaseOrder($input: CreatePurchaseOrderInput!) { createPurchaseOrder(input: $input) { id code status items { id materialId } }}`;
const 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).const DELIVERY_RECEIPTS = `query DeliveryReceipts($id: ID!, $documentNumber: String!) { stockMovements( filter: { purchaseOrderId: $id, documentNumber: $documentNumber } pagination: { page: 1, pageSize: 1 } ) { totalCount }}`;
const RECEIVE_DELIVERY = `mutation ReceiveDelivery($id: ID!, $input: ReceivePurchaseOrderInput!) { receivePurchaseOrder(id: $id, input: $input) { id code status }}`;
/** Lista materiałów w odpowiedzi FindMaterial dla rodzaju pozycji zamówienia. */const LIST_OF = { PAPER: 'papers', PAINT: 'paints', FOIL: 'foils' };
const digits = (nip) => nip.replaceAll(/\D/g, '');
/** Zamówienie zakupu w Prynt o tym numerze w systemie dostawcy (`externalRef`) albo null. */async function findOrder(number) { const { purchaseOrders } = await pryntRequest({ query: FIND_PURCHASE_ORDER, variables: { ref: number }, }); return purchaseOrders.items[0] ?? null;}
/** * Dostawca po całym NIP; 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. */async function ensureSupplier(supplier) { const nip = digits(supplier.nip); const { suppliers } = await pryntRequest({ query: FIND_SUPPLIER, variables: { nip } }); // Filtr szuka fragmentu numeru: bierzemy tylko dostawcę z całym tym samym NIP. const same = suppliers.items.filter((candidate) => candidate.nip === nip); const active = same.find((candidate) => candidate.isActive); if (active) return active.id; if (same.length > 0) { throw new Error(`Dostawca z NIP ${nip} jest w Prynt nieaktywny: przywrócić go może drukarnia`); } const { createSupplier } = await pryntRequest({ query: CREATE_SUPPLIER, variables: { input: { name: supplier.name, nip, email: supplier.email ?? null } }, idempotencyKey: `dostawca-${nip}`, }); return createSupplier.id;}
/** * Id materiału w magazynie Prynt po jego kodzie (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. */async function materialId(kind, code) { for (let page = 1; ; page += 1) { const data = await pryntRequest({ query: FIND_MATERIAL, variables: { code, page } }); const list = data[LIST_OF[kind]]; const material = list.items.find((candidate) => candidate.code === code); if (material) return material.id; if (!list.pageInfo.hasNextPage) { throw new Error(`Materiał ${code} (${kind}) nie istnieje w magazynie Prynt`); } }}
/** Czy dostawa o tym numerze WZ jest już przyjęta: ruch zamówienia z tym numerem dokumentu. */async function deliveryReceived(orderId, deliveryId) { const { stockMovements } = await pryntRequest({ query: DELIVERY_RECEIPTS, variables: { id: orderId, documentNumber: deliveryId }, }); return stockMovements.totalCount > 0;}
/** Dane nowego zamówienia zakupu: pozycje wskazują materiały, których stan podniesie dostawa. */async function orderInput(order, supplierId) { const items = []; for (const line of order.lines) { items.push({ materialType: line.kind, materialId: await materialId(line.kind, line.code), description: line.description, quantity: line.quantity, unit: line.unit, unitPrice: line.netPrice, }); } return { supplierId, currency: order.currency, expectedDeliveryAt: order.expectedDelivery ? `${order.expectedDelivery}T00:00:00.000Z` : null, notes: `Zamówienie ${order.number} z systemu dostawcy`, externalRef: order.number, items, };}
/** * Wysyła zamówienie zakupu z systemu dostawcy 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. */export async function sendPurchaseOrder(order) { let found = await findOrder(order.number); const created = found === null; if (created) { const input = await orderInput(order, await ensureSupplier(order.supplier)); const { createPurchaseOrder } = await pryntRequest({ query: CREATE_PURCHASE_ORDER, variables: { input }, idempotencyKey: `zamowienie-zakupu-${order.number}`, }); found = createPurchaseOrder; } if (found.status === 'DRAFT') { const sent = await pryntRequest({ query: SEND_PURCHASE_ORDER, variables: { id: found.id } }); found = { ...found, ...sent.updatePurchaseOrderStatus }; } return { id: found.id, code: found.code, status: found.status, created };}
/** * Przyjmuje dostawę (dokument WZ) do zamówienia zakupu o numerze `number`: 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. */export async function receiveDelivery(number, delivery) { const order = await findOrder(number); if (!order) throw new Error(`Zamówienie zakupu ${number} nie istnieje w Prynt`); if (await deliveryReceived(order.id, delivery.id)) { return { id: order.id, status: order.status, received: false }; } const items = []; for (const line of delivery.lines) { const id = await materialId(line.kind, line.code); const item = order.items.find((candidate) => candidate.materialId === id); if (!item) throw new Error(`Pozycja ${line.code} nie należy do zamówienia ${number}`); items.push({ itemId: item.id, receivedQuantity: line.quantity }); } const { receivePurchaseOrder } = await pryntRequest({ query: RECEIVE_DELIVERY, variables: { id: order.id, input: { items, documentNumber: delivery.id, deliveredAt: delivery.date ? `${delivery.date}T00:00:00.000Z` : null, }, }, idempotencyKey: `dostawa-${number}-${delivery.id}`, }); return { id: order.id, status: receivePurchaseOrder.status, received: true };}import { receiveDelivery, sendPurchaseOrder } from './purchase-order.mjs';
// order i delivery: zamówienie i WZ z systemu dostawcy w kształcie z przykładów wyżejconst sent = await sendPurchaseOrder(order); // { id, code, status: 'SENT', created }const receipt = await receiveDelivery(order.number, delivery); // { id, status, received }# purchase_order.py — Python 3.9+, klient API z przepisu „Obsługa błędów i ponowień”import re
from prynt_client import prynt_request
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 }}"""
# Lista materiałów w odpowiedzi FindMaterial dla rodzaju pozycji zamówienia.LIST_OF = {"PAPER": "papers", "PAINT": "paints", "FOIL": "foils"}
def _digits(nip): return re.sub(r"\D", "", nip)
def _find_order(number): """Zamówienie zakupu w Prynt o tym numerze w systemie dostawcy (externalRef) albo None.""" items = prynt_request(FIND_PURCHASE_ORDER, {"ref": number})["purchaseOrders"]["items"] return items[0] if items else None
def _ensure_supplier(supplier): """Dostawca po całym NIP; 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.""" nip = _digits(supplier["nip"]) found = prynt_request(FIND_SUPPLIER, {"nip": nip})["suppliers"]["items"] # Filtr szuka fragmentu numeru: bierzemy tylko dostawcę z całym tym samym NIP. same = [candidate for candidate in found if candidate["nip"] == nip] for candidate in same: if candidate["isActive"]: return candidate["id"] if same: raise ValueError(f"Dostawca z NIP {nip} jest w Prynt nieaktywny: przywrócić go może drukarnia") created = prynt_request( CREATE_SUPPLIER, {"input": {"name": supplier["name"], "nip": nip, "email": supplier.get("email")}}, idempotency_key=f"dostawca-{nip}", ) return created["createSupplier"]["id"]
def _material_id(kind, code): """Id materiału w magazynie Prynt po jego kodzie (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.""" page = 1 while True: found = prynt_request(FIND_MATERIAL, {"code": code, "page": page})[LIST_OF[kind]] for candidate in found["items"]: if candidate["code"] == code: return candidate["id"] if not found["pageInfo"]["hasNextPage"]: raise ValueError(f"Materiał {code} ({kind}) nie istnieje w magazynie Prynt") page += 1
def _delivery_received(order_id, delivery_id): """Czy dostawa o tym numerze WZ jest już przyjęta: ruch zamówienia z tym numerem dokumentu.""" variables = {"id": order_id, "documentNumber": delivery_id} return prynt_request(DELIVERY_RECEIPTS, variables)["stockMovements"]["totalCount"] > 0
def _order_input(order, supplier_id): """Dane nowego zamówienia zakupu: pozycje wskazują materiały, których stan podniesie dostawa.""" items = [ { "materialType": line["kind"], "materialId": _material_id(line["kind"], line["code"]), "description": line["description"], "quantity": line["quantity"], "unit": line["unit"], "unitPrice": line["netPrice"], } for line in order["lines"] ] expected = order.get("expectedDelivery") return { "supplierId": supplier_id, "currency": order["currency"], "expectedDeliveryAt": f"{expected}T00:00:00.000Z" if expected else None, "notes": f"Zamówienie {order['number']} z systemu dostawcy", "externalRef": order["number"], "items": items, }
def send_purchase_order(order): """Wysyła zamówienie zakupu z systemu dostawcy 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.""" found = _find_order(order["number"]) created = found is None if created: order_input = _order_input(order, _ensure_supplier(order["supplier"])) found = prynt_request( CREATE_PURCHASE_ORDER, {"input": order_input}, idempotency_key=f"zamowienie-zakupu-{order['number']}", )["createPurchaseOrder"] if found["status"] == "DRAFT": sent = prynt_request(SEND_PURCHASE_ORDER, {"id": found["id"]})["updatePurchaseOrderStatus"] found = {**found, **sent} return {"id": found["id"], "code": found["code"], "status": found["status"], "created": created}
def receive_delivery(number, delivery): """Przyjmuje dostawę (dokument WZ) do zamówienia zakupu o numerze `number`: 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.""" order = _find_order(number) if order is None: raise ValueError(f"Zamówienie zakupu {number} nie istnieje w Prynt") if _delivery_received(order["id"], delivery["id"]): return {"id": order["id"], "status": order["status"], "received": False} items = [] for line in delivery["lines"]: material = _material_id(line["kind"], line["code"]) item = next((it for it in order["items"] if it["materialId"] == material), None) if item is None: raise ValueError(f"Pozycja {line['code']} nie należy do zamówienia {number}") items.append({"itemId": item["id"], "receivedQuantity": line["quantity"]}) date = delivery.get("date") delivery_input = { "items": items, "documentNumber": delivery["id"], "deliveredAt": f"{date}T00:00:00.000Z" if date else None, } received = prynt_request( RECEIVE_DELIVERY, {"id": order["id"], "input": delivery_input}, idempotency_key=f"dostawa-{number}-{delivery['id']}", )["receivePurchaseOrder"] return {"id": order["id"], "status": received["status"], "received": True}from purchase_order import receive_delivery, send_purchase_order
# order i delivery: zamówienie i WZ z systemu dostawcy jako słownikisent = send_purchase_order(order) # {"id", "code", "status": "SENT", "created"}receipt = receive_delivery(order["number"], delivery) # {"id", "status", "received"}<?php// PurchaseOrder.php — PHP 8.1+, klient API z przepisu „Obsługa błędów i ponowień”require_once __DIR__ . '/PryntClient.php';
final class PurchaseOrder{ private const FIND_PURCHASE_ORDER = <<<'GRAPHQL' query FindPurchaseOrder($ref: String!) { purchaseOrders(filter: { externalRef: $ref }) { items { id code status items { id materialId } } } } GRAPHQL;
private const FIND_SUPPLIER = <<<'GRAPHQL' query FindSupplier($nip: String!) { suppliers(filter: { nip: $nip }) { items { id nip isActive } } } GRAPHQL;
private const CREATE_SUPPLIER = <<<'GRAPHQL' mutation CreateSupplier($input: CreateSupplierInput!) { createSupplier(input: $input) { id } } GRAPHQL;
// Filtr `code` szuka fragmentu kodu: kody rosnąco, strona po stronie, do kodu całego. private const FIND_MATERIAL = <<<'GRAPHQL' 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 } } } GRAPHQL;
private const CREATE_PURCHASE_ORDER = <<<'GRAPHQL' mutation CreatePurchaseOrder($input: CreatePurchaseOrderInput!) { createPurchaseOrder(input: $input) { id code status items { id materialId } } } GRAPHQL;
private const SEND_PURCHASE_ORDER = <<<'GRAPHQL' mutation SendPurchaseOrder($id: ID!) { updatePurchaseOrderStatus(input: { id: $id, status: SENT }) { id code status } } GRAPHQL;
// Ruchy zamówienia z tym numerem dokumentu dostawy (dokładnie ten numer, bez wielkości liter). private const DELIVERY_RECEIPTS = <<<'GRAPHQL' query DeliveryReceipts($id: ID!, $documentNumber: String!) { stockMovements( filter: { purchaseOrderId: $id, documentNumber: $documentNumber } pagination: { page: 1, pageSize: 1 } ) { totalCount } } GRAPHQL;
private const RECEIVE_DELIVERY = <<<'GRAPHQL' mutation ReceiveDelivery($id: ID!, $input: ReceivePurchaseOrderInput!) { receivePurchaseOrder(id: $id, input: $input) { id code status } } GRAPHQL;
// Lista materiałów w odpowiedzi FindMaterial dla rodzaju pozycji zamówienia. private const LIST_OF = ['PAPER' => 'papers', 'PAINT' => 'paints', 'FOIL' => 'foils'];
public function __construct(private readonly PryntClient $prynt = new PryntClient()) { }
/** * Wysyła zamówienie zakupu z systemu dostawcy 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. */ public function send(array $order): array { $found = $this->findOrder($order['number']); $created = $found === null; if ($created) { $input = $this->orderInput($order, $this->ensureSupplier($order['supplier'])); $found = $this->prynt->request(self::CREATE_PURCHASE_ORDER, ['input' => $input], "zamowienie-zakupu-{$order['number']}")['createPurchaseOrder']; } if ($found['status'] === 'DRAFT') { $sent = $this->prynt->request(self::SEND_PURCHASE_ORDER, ['id' => $found['id']]); $found = $sent['updatePurchaseOrderStatus'] + $found; } return ['id' => $found['id'], 'code' => $found['code'], 'status' => $found['status'], 'created' => $created]; }
/** * Przyjmuje dostawę (dokument WZ) do zamówienia zakupu o numerze $number: 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. */ public function receive(string $number, array $delivery): array { $order = $this->findOrder($number); if ($order === null) { throw new RuntimeException("Zamówienie zakupu {$number} nie istnieje w Prynt"); } if ($this->deliveryReceived($order['id'], $delivery['id'])) { return ['id' => $order['id'], 'status' => $order['status'], 'received' => false]; } $items = []; foreach ($delivery['lines'] as $line) { $material = $this->materialId($line['kind'], $line['code']); $item = null; foreach ($order['items'] as $candidate) { if ($candidate['materialId'] === $material) { $item = $candidate; break; } } if ($item === null) { throw new RuntimeException("Pozycja {$line['code']} nie należy do zamówienia {$number}"); } $items[] = ['itemId' => $item['id'], 'receivedQuantity' => $line['quantity']]; } $received = $this->prynt->request(self::RECEIVE_DELIVERY, ['id' => $order['id'], 'input' => [ 'items' => $items, 'documentNumber' => $delivery['id'], 'deliveredAt' => isset($delivery['date']) ? "{$delivery['date']}T00:00:00.000Z" : null, ]], "dostawa-{$number}-{$delivery['id']}")['receivePurchaseOrder']; return ['id' => $order['id'], 'status' => $received['status'], 'received' => true]; }
/** Zamówienie zakupu w Prynt o tym numerze w systemie dostawcy (`externalRef`) albo null. */ private function findOrder(string $number): ?array { $items = $this->prynt->request(self::FIND_PURCHASE_ORDER, ['ref' => $number])['purchaseOrders']['items']; return $items[0] ?? null; }
/** * Dostawca po całym NIP; 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. */ private function ensureSupplier(array $supplier): string { $nip = preg_replace('/\D/', '', $supplier['nip']); $found = $this->prynt->request(self::FIND_SUPPLIER, ['nip' => $nip])['suppliers']['items']; // Filtr szuka fragmentu numeru: bierzemy tylko dostawcę z całym tym samym NIP. $same = array_filter($found, fn (array $candidate): bool => $candidate['nip'] === $nip); foreach ($same as $candidate) { if ($candidate['isActive']) { return $candidate['id']; } } if ($same !== []) { throw new RuntimeException("Dostawca z NIP {$nip} jest w Prynt nieaktywny: przywrócić go może drukarnia"); } $input = ['name' => $supplier['name'], 'nip' => $nip, 'email' => $supplier['email'] ?? null]; return $this->prynt->request(self::CREATE_SUPPLIER, ['input' => $input], "dostawca-{$nip}")['createSupplier']['id']; }
/** * Id materiału w magazynie Prynt po jego kodzie (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. */ private function materialId(string $kind, string $code): string { for ($page = 1; ; $page++) { $list = $this->prynt->request(self::FIND_MATERIAL, ['code' => $code, 'page' => $page])[self::LIST_OF[$kind]]; foreach ($list['items'] as $candidate) { if ($candidate['code'] === $code) { return $candidate['id']; } } if (!$list['pageInfo']['hasNextPage']) { throw new RuntimeException("Materiał {$code} ({$kind}) nie istnieje w magazynie Prynt"); } } }
/** Czy dostawa o tym numerze WZ jest już przyjęta: ruch zamówienia z tym numerem dokumentu. */ private function deliveryReceived(string $orderId, string $deliveryId): bool { $movements = $this->prynt->request(self::DELIVERY_RECEIPTS, ['id' => $orderId, 'documentNumber' => $deliveryId])['stockMovements']; return $movements['totalCount'] > 0; }
/** Dane nowego zamówienia zakupu: pozycje wskazują materiały, których stan podniesie dostawa. */ private function orderInput(array $order, string $supplierId): array { $items = []; foreach ($order['lines'] as $line) { $items[] = [ 'materialType' => $line['kind'], 'materialId' => $this->materialId($line['kind'], $line['code']), 'description' => $line['description'], 'quantity' => $line['quantity'], 'unit' => $line['unit'], 'unitPrice' => $line['netPrice'], ]; } $expected = $order['expectedDelivery'] ?? null; return [ 'supplierId' => $supplierId, 'currency' => $order['currency'], 'expectedDeliveryAt' => $expected ? "{$expected}T00:00:00.000Z" : null, 'notes' => "Zamówienie {$order['number']} z systemu dostawcy", 'externalRef' => $order['number'], 'items' => $items, ]; }}<?phprequire __DIR__ . '/PurchaseOrder.php';
// $order i $delivery: zamówienie i WZ z systemu dostawcy jako tablice$purchasing = new PurchaseOrder();$sent = $purchasing->send($order); // ['id', 'code', 'status' => 'SENT', 'created']$receipt = $purchasing->receive($order['number'], $delivery); // ['id', 'status', 'received']Co dalej
Dział zatytułowany „Co dalej”- Potwierdzenie od dostawcy zapiszesz statusem
CONFIRMED, a nowy termin dostawy łatkąupdatePurchaseOrderzexpectedDeliveryAt. - Wydania na produkcję zapisujesz mutacjami
withdrawPaperiwithdrawPaintz ilością wydawaną (arkusze, gramy) orazwithdrawFoilz liczbą arkuszy i długością arkusza w milimetrach (numberOfSheets,sheetLengthMm), którą Prynt przelicza na rolki przezmetersPerRoll. API nie pozwoli zejść poniżej zera i odrzuci wydanie ponad stan błędemSTOCK_INSUFFICIENT, a wydanie papieru tylko do wyceny (isStock: false) błędemPAPER_PRICING_ONLY. - Stany i ruchy do raportów: przepis Stany magazynowe do BI.