Drukarnia rejestruje w Prynt faktury, które wystawiła za produkty zamówień, i faktury zakupu od dostawców, często z dokumentem PDF. Ten przepis przenosi je do programu księgowego: przy pierwszym uruchomieniu wszystkie, a przy kolejnych, np. co 5–15 minut, tylko nowe i zmienione — także te, do których ktoś dołączył dokument.
Czego potrzebujesz
Dział zatytułowany „Czego potrzebujesz”- Klucz z zakresami
invoices:read,prices:readifiles:read.invoices:readdaje faktury: numery, daty, typy, waluty i stawki VAT. Kwoty i kursy walut są cenami, więc jak każda cena w API wymagająprices:read; bez niego usuń je z zapytań (zapytanie z nimi API odrzuca błędemAPI_SCOPE_MISSING).files:readdaje dokument faktury (file) z podpisanym adresem do pobrania. - Klienta API z przepisu Obsługa błędów i ponowień. Kod niżej wysyła żądania przez niego.
- Miejsce na dwa znaczniki synchronizacji: faktury sprzedaży i faktury zakupu.
Synchronizacja
Dział zatytułowany „Synchronizacja”Listy productInvoices i purchaseInvoices mają filtr updatedFrom i sortowanie UPDATED_AT.
updatedAt faktury przesuwa każda jej zmiana w Prynt oraz dołączenie jej dokumentu (także nowego
w miejsce poprzedniego) albo jego usunięcie; zapis faktury bez zmian go nie przesuwa.
-
Pierwsza synchronizacja pobiera wszystkie faktury:
updatedFromjest wtedy pusty. Jeśli księgujesz tylko od pewnej daty, dodaj filtrissuedFrom(ta sama nazwa na obu listach). -
Zapisuj fakturę po
id: wstaw albo zaktualizuj. Faktura zmieniona w Prynt po zaksięgowaniu wraca w kolejnej porcji — sprawdź u siebie, czy już ją zaksięgowałeś, zanim ją nadpiszesz. -
Kolejną porcję zaczynaj od
updatedAtostatniej faktury, a po udanym końcu zapisz znaczniki. Następna synchronizacja zaczyna od każdego znacznika minus 5 minut. Szczegóły algorytmu: Paginacja, filtry i synchronizacja.
Faktury sprzedaży
Dział zatytułowany „Faktury sprzedaży”Faktura sprzedaży (ProductInvoice) dotyczy jednego produktu zamówienia (productId, zamówienie
— orderId); wszystkie faktury zamówienia pobierzesz filtrem orderId. Faktury wystawia program księgowy albo system
sprzedaży, a Prynt je rejestruje — dlatego API je tylko odczytuje.
- Typ (
type):VAT,PROFORMA(wezwanie do zapłaty, nie dokument księgowy — zwykle go pomijasz) alboCORRECTION. Korekta ma kwoty dodatnie jak każda faktura, a zmniejsza zafakturowaną wartość produktu: kod przekazuje ją ze znakiem (sign= -1). - Kwoty (
netAmount,vatAmount,grossAmount) są w walucie faktury (currencyCode) — walucie zamówienia.vatRateto stawka jako ułamek (0.23). - Waluta obca:
exchangeRateto kurs do waluty domowej drukarni, domyślnie z ostatniej tabeli NBP opublikowanej nie później niż dzień przed datą wystawienia (jej numer wrateTableNo).rateDateto ten dzień przed datą wystawienia, kalendarzowy, także gdy wypada w weekend, a nie dzień notowania. Kurs wpisany ręcznie przez drukarnię marateTableNo: null. Kwoty w walucie domowej tonetHome,vatHomeigrossHome. - Sprzedawca to podmiot rozliczeniowy drukarni (
billingEntityId): jej firmy, z danymi do faktury, odczytasz listąbillingEntitiesz zakresemreference:read. Nabywcę wskazuje zamówienie faktury (orderId): jeśli synchronizujesz też zamówienia (przepis Zamówienia do księgowości), połączysz je u siebie poorderId; inaczej odczytasz zamówienie zapytaniemorder(id:)— raz na zamówienie, nie na każdą fakturę (orders:read, a dane klienta —clients:read).
Faktury zakupu
Dział zatytułowany „Faktury zakupu”Faktura zakupu (PurchaseInvoice) to faktura dostawcy (supplierId) z datą invoiceDate i
kwotami netto i brutto (amountNet, amountGross) w walucie currency — pola nazywają się inaczej
niż w fakturze sprzedaży (issueDate, netAmount, grossAmount, currencyCode), filtry obu list
tak samo. Nazwę i NIP dostawcy odczytasz polem supplier albo zapytaniem supplier(id:) z
zakresem warehouse:read.
Fakturę usuniętą w Prynt synchronizacja przyrostowa pominie. Raz na dobę albo przed zamknięciem
miesiąca pobierz pełne listy bez updatedFrom (np. z filtrem daty wystawienia) i porównaj z tym,
co zaksięgowałeś.
Funkcja synchronizacji przyjmuje znaczniki z poprzedniego razu (pusty obiekt przy pierwszym), zapis faktury sprzedaży i zapis faktury zakupu, a zwraca nowe znaczniki. Zapisz je dopiero po udanym końcu: przerwana synchronizacja powtórzy się wtedy od poprzednich.
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żdą fakturę
wypisuje jako jedną linię JSON ({"sale": …} albo {"purchase": …}), a znaczniki trzyma w pliku
stan-synchronizacji-faktur.json.
#!/usr/bin/env bash# sync-invoices.sh — jedna synchronizacja z księgowością (curl, jq; prynt-call.sh z przepisu „Obsługa# błędów i ponowień”): faktury sprzedaży produktów i faktury zakupu dodane albo zmienione od# poprzedniej synchronizacji. Każdą fakturę wypisuje jako linię JSON — {"sale": …} albo# {"purchase": …} — którą zapisujesz u siebie po `id`: ./sync-invoices.sh >>faktury.ndjson# Znaczniki trzyma w pliku stan-synchronizacji-faktur.json i zapisuje je dopiero na końcu.set -euo pipefail
state_file=stan-synchronizacji-faktur.jsonstate=$(cat "$state_file" 2>/dev/null || echo '{}')body=$(mktemp) result=$(mktemp)trap 'rm -f "$body" "$result"' EXIT
# Faktury sprzedaży produktów i faktury zakupu: kwoty z prices:read, dokument (PDF) z files:read.# 40 faktur na stronę: z dokumentem mieści się w limicie kosztu zapytania.sales_query='query ChangedSalesInvoices($from: DateTime, $page: Int!) { productInvoices( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 40 } ) { items { id invoiceNumber type issueDate dueDate productId orderId billingEntityId currencyCode vatRate netAmount vatAmount grossAmount exchangeRate rateDate rateTableNo netHome vatHome grossHome updatedAt file { originalFilename mimeType downloadUrl } } pageInfo { hasNextPage } }}'purchases_query='query ChangedPurchaseInvoices($from: DateTime, $page: Int!) { purchaseInvoices( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 40 } ) { items { id invoiceNumber invoiceDate supplierId currency amountNet amountGross updatedAt file { originalFilename mimeType downloadUrl } } pageInfo { hasNextPage } }}'
# Pobiera listę $1 zapytaniem $2 od znacznika $3 (minus 5 minut zapasu) w kolejności zmiany; każdą# fakturę przepuszcza przez filtr jq $4 i wypisuje. Na stdout na końcu: nowy znacznik.pull() { local field=$1 query=$2 since=$3 row=$4 from=null latest=$3 page=1 last if [[ -n $since ]]; then from=$(jq -n --arg since "$since" '$since | capture("^(?<time>[^.]+?)(?<ms>\\.[0-9]+)?Z$") | ((.time + "Z") | fromdateiso8601 - 300 | todateiso8601 | rtrimstr("Z")) + (.ms // ".000") + "Z"') fi while :; 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 --arg field "$field" ".data[\$field].items[] | $row" "$result" >&3 # 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" --arg field "$field" \ '[$latest, .data[$field].items[].updatedAt] | map(select(. != "")) | max // ""' "$result") [[ $(jq -r --arg field "$field" '.data[$field].pageInfo.hasNextPage' "$result") == true ]] || break last=$(jq -c --arg field "$field" '.data[$field].items[-1].updatedAt' "$result") # Dalej czytamy od czasu ostatniej faktury, nie od kolejnego numeru strony; kolejną stronę # bierzemy tylko wtedy, gdy cała strona ma ten sam czas. if [[ $last == "$from" ]]; then page=$((page + 1)) else from=$last page=1 fi done echo "$latest"}
cursor() { jq -r --arg name "$1" '.[$name] // ""' <<<"$state"; }
exec 3>&1# Faktura korygująca ma kwoty dodatnie jak każda inna, a w Prynt zmniejsza zafakturowaną wartość# produktu: do księgowości przekazujemy kwoty ze znakiem (`sign`: -1 dla korekty).sales=$(pull productInvoices "$sales_query" "$(cursor sales)" \ '{ sale: (. + { sign: (if .type == "CORRECTION" then -1 else 1 end) }) }')purchases=$(pull purchaseInvoices "$purchases_query" "$(cursor purchases)" '{ purchase: . }')
jq -n --arg sales "$sales" --arg purchases "$purchases" \ '{ sales: $sales, purchases: $purchases } | with_entries(select(.value != ""))' >"$state_file"./sync-invoices.sh >>faktury.ndjson # wczytaj linie do księgowości: po id// sync-invoices.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;
// Faktury sprzedaży produktów i faktury zakupu: kwoty z prices:read, dokument (PDF) z files:read.// 40 faktur na stronę: z dokumentem mieści się w limicie kosztu zapytania.const INVOICE_LISTS = { productInvoices: `query ChangedSalesInvoices($from: DateTime, $page: Int!) { productInvoices( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 40 } ) { items { id invoiceNumber type issueDate dueDate productId orderId billingEntityId currencyCode vatRate netAmount vatAmount grossAmount exchangeRate rateDate rateTableNo netHome vatHome grossHome updatedAt file { originalFilename mimeType downloadUrl } } pageInfo { hasNextPage } }}`, purchaseInvoices: `query ChangedPurchaseInvoices($from: DateTime, $page: Int!) { purchaseInvoices( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 40 } ) { items { id invoiceNumber invoiceDate supplierId currency amountNet amountGross updatedAt file { originalFilename mimeType downloadUrl } } pageInfo { hasNextPage } }}`,};
/** * Faktura korygująca ma kwoty dodatnie jak każda inna, a w Prynt zmniejsza zafakturowaną wartość * produktu: do księgowości przekazujemy kwoty ze znakiem (`sign`: -1 dla korekty). */const signed = (invoice) => ({ ...invoice, sign: invoice.type === 'CORRECTION' ? -1 : 1 });
const later = (a, b) => (a === null || Date.parse(b) > Date.parse(a) ? b : a);const minusOverlap = (since) => since === null ? null : new Date(Date.parse(since) - OVERLAP_MS).toISOString();
/** * Pobiera listę `field` od `since` (minus zapas) w kolejności zmiany i przekazuje każdą fakturę do * `save`. Zwraca najpóźniejszy `updatedAt`, jaki widziała — znacznik do następnej synchronizacji. */async function pull(field, since, save) { let from = minusOverlap(since); let latest = since; for (let page = 1; ; ) { const data = await pryntRequest({ query: INVOICE_LISTS[field], variables: { from, page } }); const list = data[field]; for (const invoice of list.items) { await save(invoice); latest = later(latest, invoice.updatedAt); } if (!list.pageInfo.hasNextPage) return latest; const last = list.items.at(-1).updatedAt; // Dalej czytamy od czasu ostatniej faktury, a nie od kolejnego numeru strony; kolejną stronę // bierzemy tylko wtedy, gdy cała strona ma ten sam czas. if (last === from) { page += 1; } else { from = last; page = 1; } }}
/** * Jedna synchronizacja z księgowością: faktury sprzedaży (`saveSale`) i zakupu (`savePurchase`) * dodane albo zmienione od poprzedniej — także te, do których dołączono dokument. Zapisuj je po * `id` (wstaw albo zaktualizuj). `state` to znaczniki z poprzedniej synchronizacji (`{}` przy * pierwszej); zwraca nowe — zapisz je dopiero po udanym końcu. */export async function syncInvoices(state, saveSale, savePurchase) { return { sales: await pull('productInvoices', state.sales ?? null, (invoice) => saveSale(signed(invoice)), ), purchases: await pull('purchaseInvoices', state.purchases ?? null, savePurchase), };}import { readFile, writeFile } from 'node:fs/promises';import { syncInvoices } from './sync-invoices.mjs';
const STATE = 'stan-synchronizacji-faktur.json';const state = JSON.parse(await readFile(STATE, 'utf8').catch(() => '{}'));const next = await syncInvoices( state, async (invoice) => bookSale(invoice), // Twoja funkcja: zapis po invoice.id, kwoty × invoice.sign async (invoice) => bookPurchase(invoice), // Twoja funkcja: zapis po invoice.id);await writeFile(STATE, JSON.stringify(next));# sync_invoices.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)
# Faktury sprzedaży produktów i faktury zakupu: kwoty z prices:read, dokument (PDF) z files:read.# 40 faktur na stronę: z dokumentem mieści się w limicie kosztu zapytania.INVOICE_LISTS = { "productInvoices": """query ChangedSalesInvoices($from: DateTime, $page: Int!) { productInvoices( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 40 } ) { items { id invoiceNumber type issueDate dueDate productId orderId billingEntityId currencyCode vatRate netAmount vatAmount grossAmount exchangeRate rateDate rateTableNo netHome vatHome grossHome updatedAt file { originalFilename mimeType downloadUrl } } pageInfo { hasNextPage } }}""", "purchaseInvoices": """query ChangedPurchaseInvoices($from: DateTime, $page: Int!) { purchaseInvoices( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 40 } ) { items { id invoiceNumber invoiceDate supplierId currency amountNet amountGross updatedAt file { originalFilename mimeType downloadUrl } } pageInfo { hasNextPage } }}""",}
def _signed(invoice): """Faktura korygująca ma kwoty dodatnie jak każda inna, a w Prynt zmniejsza zafakturowaną wartość produktu: do księgowości przekazujemy kwoty ze znakiem (`sign`: -1 dla korekty).""" return {**invoice, "sign": -1 if invoice["type"] == "CORRECTION" else 1}
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 _pull(field, since, save): """Pobiera listę `field` od `since` (minus zapas) w kolejności zmiany i przekazuje każdą fakturę do `save`. Zwraca najpóźniejszy `updatedAt` — znacznik do następnej synchronizacji.""" start = None if since is None else _iso(_instant(since) - OVERLAP) latest, page = since, 1 while True: result = prynt_request(INVOICE_LISTS[field], {"from": start, "page": page})[field] for invoice in result["items"]: save(invoice) if latest is None or _instant(invoice["updatedAt"]) > _instant(latest): latest = invoice["updatedAt"] if not result["pageInfo"]["hasNextPage"]: return latest last = result["items"][-1]["updatedAt"] # Dalej czytamy od czasu ostatniej faktury, nie od kolejnego numeru strony; kolejną # stronę bierzemy tylko wtedy, gdy cała strona ma ten sam czas. if last == start: page += 1 else: start, page = last, 1
def sync_invoices(state, save_sale, save_purchase): """Jedna synchronizacja z księgowością: faktury sprzedaży (`save_sale`) i zakupu (`save_purchase`) dodane albo zmienione od poprzedniej — zapisuj je po `id`. `state` to znaczniki z poprzedniej synchronizacji ({} przy pierwszej); zwraca nowe — zapisz je dopiero po udanym końcu.""" return { "sales": _pull("productInvoices", state.get("sales"), lambda invoice: save_sale(_signed(invoice))), "purchases": _pull("purchaseInvoices", state.get("purchases"), save_purchase), }import jsonfrom pathlib import Path
from sync_invoices import sync_invoices
state_file = Path("stan-synchronizacji-faktur.json")state = json.loads(state_file.read_text()) if state_file.exists() else {}# book_sale i book_purchase: Twoje funkcje zapisu w księgowości (po id)next_state = sync_invoices(state, book_sale, book_purchase)state_file.write_text(json.dumps(next_state))<?php// SyncInvoices.php — PHP 8.1+, klient API z przepisu „Obsługa błędów i ponowień”require_once __DIR__ . '/PryntClient.php';
final class SyncInvoices{ // Zapas na zmiany zapisane tuż przed poprzednią synchronizacją, a widoczne dopiero po niej. private const OVERLAP = 'PT5M';
// Faktury sprzedaży produktów i faktury zakupu: kwoty z prices:read, dokument (PDF) z files:read. // 40 faktur na stronę: z dokumentem mieści się w limicie kosztu zapytania. private const INVOICE_LISTS = [ 'productInvoices' => <<<'GRAPHQL' query ChangedSalesInvoices($from: DateTime, $page: Int!) { productInvoices( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 40 } ) { items { id invoiceNumber type issueDate dueDate productId orderId billingEntityId currencyCode vatRate netAmount vatAmount grossAmount exchangeRate rateDate rateTableNo netHome vatHome grossHome updatedAt file { originalFilename mimeType downloadUrl } } pageInfo { hasNextPage } } } GRAPHQL, 'purchaseInvoices' => <<<'GRAPHQL' query ChangedPurchaseInvoices($from: DateTime, $page: Int!) { purchaseInvoices( filter: { updatedFrom: $from } sort: { field: UPDATED_AT, direction: ASC } pagination: { page: $page, pageSize: 40 } ) { items { id invoiceNumber invoiceDate supplierId currency amountNet amountGross updatedAt file { originalFilename mimeType downloadUrl } } pageInfo { hasNextPage } } } GRAPHQL, ];
public function __construct(private readonly PryntClient $prynt = new PryntClient()) { }
/** * Jedna synchronizacja z księgowością: faktury sprzedaży ($saveSale) i zakupu ($savePurchase) * dodane albo zmienione od poprzedniej — zapisuj je po `id`. $state to znaczniki z poprzedniej * synchronizacji ([] przy pierwszej); zwraca nowe — zapisz je dopiero po udanym końcu. */ public function sync(array $state, callable $saveSale, callable $savePurchase): array { return [ 'sales' => $this->pull('productInvoices', $state['sales'] ?? null, fn (array $invoice) => $saveSale(self::signed($invoice))), 'purchases' => $this->pull('purchaseInvoices', $state['purchases'] ?? null, $savePurchase), ]; }
/** * Faktura korygująca ma kwoty dodatnie jak każda inna, a w Prynt zmniejsza zafakturowaną * wartość produktu: do księgowości przekazujemy kwoty ze znakiem (`sign`: -1 dla korekty). */ private static function signed(array $invoice): array { return $invoice + ['sign' => $invoice['type'] === 'CORRECTION' ? -1 : 1]; }
/** * Pobiera listę $field od $since (minus zapas) w kolejności zmiany i przekazuje każdą fakturę * do $save. Zwraca najpóźniejszy `updatedAt` — znacznik do następnej synchronizacji. */ private function pull(string $field, ?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) { $list = $this->prynt->request(self::INVOICE_LISTS[$field], ['from' => $from, 'page' => $page])[$field]; foreach ($list['items'] as $invoice) { $save($invoice); if ($latest === null || new DateTimeImmutable($invoice['updatedAt']) > new DateTimeImmutable($latest)) { $latest = $invoice['updatedAt']; } } if (!$list['pageInfo']['hasNextPage']) { return $latest; } $last = end($list['items'])['updatedAt']; // Dalej czytamy od czasu ostatniej faktury, nie od kolejnego numeru strony; kolejną // stronę bierzemy tylko wtedy, gdy cała strona ma ten sam czas. if ($last === $from) { $page++; } else { $from = $last; $page = 1; } } }}<?phprequire __DIR__ . '/SyncInvoices.php';
$stateFile = __DIR__ . '/stan-synchronizacji-faktur.json';$state = is_file($stateFile) ? json_decode(file_get_contents($stateFile), true) : [];$next = (new SyncInvoices())->sync( $state, fn (array $invoice) => bookSale($invoice), // Twoja funkcja: zapis po id, kwoty × $invoice['sign'] fn (array $invoice) => bookPurchase($invoice), // Twoja funkcja: zapis po id);file_put_contents($stateFile, json_encode($next));Co dalej
Dział zatytułowany „Co dalej”- Płatności i rozrachunki: przepis Zamówienia do księgowości przenosi zamówienia z płatnościami.
- Stawki VAT i waluty jako słowniki:
vatRatesicurrenciesz zakresemreference:read.