System księgowy albo ERP potrzebuje zamówień z cenami, kosztami dodatkowymi i płatnościami, a potem ich zmian. Ten przepis pobiera przy pierwszym uruchomieniu wszystkie zamówienia, a przy kolejnych, np. co 5–15 minut, tylko te, które zmieniły się od poprzedniego razu.
Czego potrzebujesz
Dział zatytułowany „Czego potrzebujesz”- Klucz z zakresami
orders:readiprices:read. Bezprices:readpola z kwotami (ceny produktów i operacji, narzuty, koszty dodatkowe, kwoty płatności, cena transportu i kursy walut) są niedostępne, a zapytanie, które ich używa, API odrzuca błędemAPI_SCOPE_MISSING. - Klienta API z przepisu Obsługa błędów i ponowień. Kod niżej wysyła żądania przez niego.
- Miejsce na znacznik synchronizacji, np. plik albo wiersz w bazie: jeden ciąg znaków z datą.
Jak to działa
Dział zatytułowany „Jak to działa”Order.updatedAt zmienia się przy każdej zmianie zamówienia albo czegokolwiek w nim: produktów, ich
elementów i operacji, płatności, kosztów dodatkowych, plików, osoby kontaktowej, dostaw i danych do
faktury. Filtr updatedFrom zwraca zamówienia zmienione w tej chwili lub później, a sortowanie
UPDATED_AT rosnąco ustawia je w kolejności zmian.
-
Pierwsza synchronizacja pobiera wszystkie zamówienia, także archiwalne:
updatedFromjest wtedy pusty. -
Zapisuj zamówienie po
id: wstaw je albo zastąp w całości, razem z listą produktów, kosztów i płatności. Produkt albo płatność usunięte w Prynt po prostu znikają z listy zamówienia. -
Kolejną porcję zaczynaj od
updatedAtostatniego pobranego zamówienia, nie od następnego numeru strony. Zamówienie zmienione w trakcie przeskakuje na koniec listy, a przy numerach stron przesunęłoby pozostałe i jedno z nich by umknęło. Kolejną stronę z tym samymupdatedFrombierz tylko wtedy, gdy cała strona ma ten samupdatedAt. -
Po udanym końcu zapisz znacznik: najpóźniejszy
updatedAt, jaki widziałeś. Następna synchronizacja zaczyna od znacznika minus 5 minut. Zapas łapie zmiany zapisane tuż przed poprzednią synchronizacją, a widoczne w API dopiero po niej.
Zamówienie pobrane dwa razy niczego nie psuje, bo zapisujesz je po id. Przerwana synchronizacja
nie zapisuje znacznika, więc następna po prostu powtórzy się od poprzedniego.
updatedAt przesuwa tylko prawdziwa zmiana. Jeśli Twój system odsyła do Prynt te same wartości,
które pobrał (np. przy synchronizacji w obie strony), zamówienie nie wróci w następnej porcji. Zmiana
zapisana przez Twoją integrację wróci tak samo jak zmiana z Prynt.
Kwoty w odpowiedzi
Dział zatytułowany „Kwoty w odpowiedzi”- Produkt:
priceto cena netto całej pozycji (wszystkich sztuk razem),priceMarkupto narzut kwotowy, więc wartość netto produktu toprice + priceMarkup.taxRatejest ułamkiem, np.0.23dla 23%. - Koszty dodatkowe:
amountnetto itaxRatejako ułamek. Koszt zproductIddotyczy produktu, a bez niego całego zamówienia. - Waluta: kwoty zamówienia są w jego walucie (
currencyId), aexchangeRateprzelicza je na walutę drukarni. Na starszych zamówieniach oba pola są puste: to waluta drukarni. - Płatności:
amountw walucie płatności (currency), z datąpaidAt. - Pozycje bez faktury: produkt z
noInvoice: truedrukarnia oznaczyła jako niefakturowany.
Zapytanie pobiera 25 zamówień na stronę: z produktami, kosztami i płatnościami mieści się w limicie kosztu zapytania. Jeśli dodasz pola z listami, zmniejsz stronę. Zobacz Limity.
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. Każde zamówienie
wypisuje jako jedną linię JSON, a znacznik trzyma w pliku ostatnia-synchronizacja.txt.
#!/usr/bin/env bash# sync-orders.sh — pobiera zamówienia zmienione od poprzedniej synchronizacji (curl, jq; prynt-call.sh# z przepisu „Obsługa błędów i ponowień”). Każde zamówienie wypisuje jako jedną linię JSON, którą# zapisujesz u siebie po `id`: ./sync-orders.sh >>zamowienia.ndjson# Znacznik synchronizacji trzyma w pliku ostatnia-synchronizacja.txt i zapisuje go dopiero na końcu.set -euo pipefail
cursor_file=ostatnia-synchronizacja.txt# 25 zamówień na stronę: z produktami, kosztami i płatnościami mieści się w limicie kosztu zapytania.query='query ChangedOrders($from: DateTime, $page: Int!) { orders( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 25 } ) { items { id displayId status updatedAt clientId billingEntityId currencyId exchangeRate transportPrice paymentMethod paymentStatus paymentDueDate invoice { companyName nip street zip city country } products { id number name quantity unit price priceMarkup taxRate noInvoice } additionalCosts { id productId name amount taxRate } payments { id amount currency paidAt method reference } } pageInfo { hasNextPage } }}'
since=$(cat "$cursor_file" 2>/dev/null || true)from=null # wartość JSON zmiennej $from: null przy pierwszej synchronizacji (wszystkie zamówienia)if [[ -n $since ]]; then # 5 minut zapasu na zmiany zapisane tuż przed poprzednią synchronizacją, a widoczne po niej. from=$(jq -n --arg since "$since" '$since | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601 - 300 | todateiso8601')fi
body=$(mktemp) result=$(mktemp)trap 'rm -f "$body" "$result"' EXITlatest=$since page=1while :; do jq -n --arg query "$query" --argjson from "$from" --argjson page "$page" \ '{ query: $query, variables: { from: $from, page: $page } }' >"$body" ./prynt-call.sh "$body" >"$result" jq -c '.data.orders.items[]' "$result" # Czasy z API mają jeden format (np. 2026-10-05T10:41:02.000Z), więc porównujemy je jako tekst. latest=$(jq -r --arg latest "$latest" \ '[$latest, .data.orders.items[].updatedAt] | map(select(. != "")) | max // ""' "$result") [[ $(jq -r '.data.orders.pageInfo.hasNextPage' "$result") == true ]] || break last=$(jq -c '.data.orders.items[-1].updatedAt' "$result") # Dalej czytamy od czasu ostatniego zamówienia, nie od kolejnego numeru strony; kolejną stronę # bierzemy tylko wtedy, gdy cała strona ma ten sam czas zmiany. if [[ $last == "$from" ]]; then page=$((page + 1)) else from=$last page=1 fidoneif [[ -n $latest ]]; then echo "$latest" >"$cursor_file"; fi./sync-orders.sh >>zamowienia.ndjson # wczytaj linie do księgowości, zapisując po id// sync-orders.mjs — Node.js 18+, klient API z przepisu „Obsługa błędów i ponowień”import { pryntRequest } from './prynt-client.mjs';
// Zapas na zmiany zapisane tuż przed poprzednią synchronizacją, a widoczne dopiero po niej.const OVERLAP_MS = 5 * 60 * 1000;
// 25 zamówień na stronę: z produktami, kosztami i płatnościami mieści się w limicie kosztu zapytania.const CHANGED_ORDERS = `query ChangedOrders($from: DateTime, $page: Int!) { orders( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 25 } ) { items { id displayId status updatedAt clientId billingEntityId currencyId exchangeRate transportPrice paymentMethod paymentStatus paymentDueDate invoice { companyName nip street zip city country } products { id number name quantity unit price priceMarkup taxRate noInvoice } additionalCosts { id productId name amount taxRate } payments { id amount currency paidAt method reference } } pageInfo { hasNextPage } }}`;
const later = (a, b) => (a === null || Date.parse(b) > Date.parse(a) ? b : a);
/** * Pobiera zamówienia zmienione od `since` (ISO 8601; `null` przy pierwszej synchronizacji: wszystkie) * i przekazuje każde do `save`, które zapisuje je po `id` (wstaw albo zaktualizuj). Zwraca znacznik * do następnej synchronizacji: najpóźniejszy `updatedAt`, jaki widziała. Zapisz go dopiero po * udanym końcu, wtedy przerwana synchronizacja po prostu powtórzy się od poprzedniego znacznika. */export async function syncOrders(since, save) { let from = since === null ? null : new Date(Date.parse(since) - OVERLAP_MS).toISOString(); let latest = since; for (let page = 1; ; ) { const { orders } = await pryntRequest({ query: CHANGED_ORDERS, variables: { from, page } }); for (const order of orders.items) { await save(order); latest = later(latest, order.updatedAt); } if (!orders.pageInfo.hasNextPage) return latest; const last = orders.items.at(-1).updatedAt; // Dalej czytamy od czasu ostatniego zamówienia, a nie od kolejnego numeru strony: zamówienie // zmienione w trakcie przeskakuje na koniec listy, a przy numerach stron przesunęłoby pozostałe // i jedno z nich by umknęło. Tylko gdy cała strona ma ten sam czas zmiany, bierzemy kolejną. if (last === from) { page += 1; } else { from = last; page = 1; } }}import { readFile, writeFile } from 'node:fs/promises';import { syncOrders } from './sync-orders.mjs';
const MARKER = 'ostatnia-synchronizacja.txt';const since = (await readFile(MARKER, 'utf8').catch(() => '')).trim() || null;const next = await syncOrders(since, async (order) => { await saveInAccounting(order); // Twoja funkcja: wstaw albo zastąp zamówienie po order.id});if (next) await writeFile(MARKER, next);# sync_orders.py — Python 3.9+, klient API z przepisu „Obsługa błędów i ponowień”from datetime import datetime, timedelta, timezone
from prynt_client import prynt_request
# Zapas na zmiany zapisane tuż przed poprzednią synchronizacją, a widoczne dopiero po niej.OVERLAP = timedelta(minutes=5)
# 25 zamówień na stronę: z produktami, kosztami i płatnościami mieści się w limicie kosztu zapytania.CHANGED_ORDERS = """query ChangedOrders($from: DateTime, $page: Int!) { orders( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 25 } ) { items { id displayId status updatedAt clientId billingEntityId currencyId exchangeRate transportPrice paymentMethod paymentStatus paymentDueDate invoice { companyName nip street zip city country } products { id number name quantity unit price priceMarkup taxRate noInvoice } additionalCosts { id productId name amount taxRate } payments { id amount currency paidAt method reference } } pageInfo { hasNextPage } }}"""
def _instant(value): return datetime.fromisoformat(value.replace("Z", "+00:00"))
def _iso(instant): return instant.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def sync_orders(since, save): """Pobiera zamówienia zmienione od `since` (ISO 8601; None przy pierwszej synchronizacji: wszystkie) i przekazuje każde do `save`, które zapisuje je po `id` (wstaw albo zaktualizuj). Zwraca znacznik do następnej synchronizacji: najpóźniejszy `updatedAt`, jaki widziała. Zapisz go dopiero po udanym końcu.""" start = None if since is None else _iso(_instant(since) - OVERLAP) latest, page = since, 1 while True: orders = prynt_request(CHANGED_ORDERS, {"from": start, "page": page})["orders"] for order in orders["items"]: save(order) if latest is None or _instant(order["updatedAt"]) > _instant(latest): latest = order["updatedAt"] if not orders["pageInfo"]["hasNextPage"]: return latest last = orders["items"][-1]["updatedAt"] # Dalej czytamy od czasu ostatniego zamówienia, nie od kolejnego numeru strony; kolejną # stronę bierzemy tylko wtedy, gdy cała strona ma ten sam czas zmiany. if last == start: page += 1 else: start, page = last, 1from pathlib import Path
from sync_orders import sync_orders
marker = Path("ostatnia-synchronizacja.txt")# Pusty plik znaczy to samo co brak pliku: pierwsza synchronizacja.since = (marker.read_text().strip() or None) if marker.exists() else None# save_in_accounting: Twoja funkcja, wstawia albo zastępuje zamówienie po order["id"]next_since = sync_orders(since, save_in_accounting)if next_since: marker.write_text(next_since)<?php// SyncOrders.php — PHP 8.1+, klient API z przepisu „Obsługa błędów i ponowień”require_once __DIR__ . '/PryntClient.php';
final class SyncOrders{ // Zapas na zmiany zapisane tuż przed poprzednią synchronizacją, a widoczne dopiero po niej. private const OVERLAP = 'PT5M';
// 25 zamówień na stronę: z produktami, kosztami i płatnościami mieści się w limicie kosztu. private const CHANGED_ORDERS = <<<'GRAPHQL' query ChangedOrders($from: DateTime, $page: Int!) { orders( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 25 } ) { items { id displayId status updatedAt clientId billingEntityId currencyId exchangeRate transportPrice paymentMethod paymentStatus paymentDueDate invoice { companyName nip street zip city country } products { id number name quantity unit price priceMarkup taxRate noInvoice } additionalCosts { id productId name amount taxRate } payments { id amount currency paidAt method reference } } pageInfo { hasNextPage } } } GRAPHQL;
public function __construct(private readonly PryntClient $prynt = new PryntClient()) { }
/** * Pobiera zamówienia zmienione od $since (ISO 8601; null przy pierwszej synchronizacji: * wszystkie) i przekazuje każde do $save, które zapisuje je po `id`. Zwraca znacznik do * następnej synchronizacji (najpóźniejszy `updatedAt`); zapisz go dopiero po udanym końcu. */ public function sync(?string $since, callable $save): ?string { $from = $since === null ? null : (new DateTimeImmutable($since)) ->sub(new DateInterval(self::OVERLAP)) ->setTimezone(new DateTimeZone('UTC')) ->format('Y-m-d\TH:i:s.v\Z'); $latest = $since; $page = 1; while (true) { $orders = $this->prynt->request(self::CHANGED_ORDERS, ['from' => $from, 'page' => $page])['orders']; foreach ($orders['items'] as $order) { $save($order); if ($latest === null || new DateTimeImmutable($order['updatedAt']) > new DateTimeImmutable($latest)) { $latest = $order['updatedAt']; } } if (!$orders['pageInfo']['hasNextPage']) { return $latest; } $last = end($orders['items'])['updatedAt']; // Dalej czytamy od czasu ostatniego zamówienia, nie od kolejnego numeru strony; kolejną // stronę bierzemy tylko wtedy, gdy cała strona ma ten sam czas zmiany. if ($last === $from) { $page++; } else { $from = $last; $page = 1; } } }}<?phprequire __DIR__ . '/SyncOrders.php';
$marker = __DIR__ . '/ostatnia-synchronizacja.txt';// Pusty plik znaczy to samo co brak pliku: pierwsza synchronizacja.$since = is_file($marker) ? (trim(file_get_contents($marker)) ?: null) : null;$next = (new SyncOrders())->sync($since, function (array $order): void { saveInAccounting($order); // Twoja funkcja: wstaw albo zastąp zamówienie po $order['id']});if ($next !== null) { file_put_contents($marker, $next);}Co dalej
Dział zatytułowany „Co dalej”- Zamówienia tylko w wybranych statusach odfiltruj u siebie. Filtr
statusInw zapytaniu pominąłby zamówienie, które z takiego statusu wyszło, i Twoja kopia zostałaby nieaktualna. - Wspólne zasady list i synchronizacji: Paginacja, filtry i synchronizacja.