Przejdź do głównej zawartości

Faktury do księgowości

Dla: Programista

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.

  • Klucz z zakresami invoices:read, prices:read i files:read. invoices:read daje 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łędem API_SCOPE_MISSING). files:read daje 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.

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.

  1. Pierwsza synchronizacja pobiera wszystkie faktury: updatedFrom jest wtedy pusty. Jeśli księgujesz tylko od pewnej daty, dodaj filtr issuedFrom (ta sama nazwa na obu listach).

  2. 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.

  3. Kolejną porcję zaczynaj od updatedAt ostatniej 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.

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) albo CORRECTION. 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. vatRate to stawka jako ułamek (0.23).
  • Waluta obca: exchangeRate to kurs do waluty domowej drukarni, domyślnie z ostatniej tabeli NBP opublikowanej nie później niż dzień przed datą wystawienia (jej numer w rateTableNo). rateDate to ten dzień przed datą wystawienia, kalendarzowy, także gdy wypada w weekend, a nie dzień notowania. Kurs wpisany ręcznie przez drukarnię ma rateTableNo: null. Kwoty w walucie domowej to netHome, vatHome i grossHome.
  • Sprzedawca to podmiot rozliczeniowy drukarni (billingEntityId): jej firmy, z danymi do faktury, odczytasz listą billingEntities z zakresem reference: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 po orderId; inaczej odczytasz zamówienie zapytaniem order(id:) — raz na zamówienie, nie na każdą fakturę (orders:read, a dane klienta — clients:read).

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.

sync-invoices.sh
#!/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.json
state=$(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"
Okno terminala
./sync-invoices.sh >>faktury.ndjson # wczytaj linie do księgowości: po id
  • 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: vatRates i currencies z zakresem reference:read.