Zum Inhalt springen
Dokumentationsmenü

Leitfäden

Mit Ihrem ERP synchronisieren

Eine vollständige Nachtprodukt- und Kundensynchronisation sowie ein Bestellabholer, der neue Bestellungen in Ihr eigenes System zieht.

Dieser Leitfaden erstellt die beiden Jobs, die die meisten Lieferanten benötigen:

  1. Nächtlicher Sync: Produkte und Kunden aus Ihrem ERP in TradeCatalog übertragen.
  2. Bestellpoller: Alle paar Minuten neue Bestellungen in Ihr ERP abrufen und als bestätigt markieren.

Sie benötigen einen Lese- und Schreib-API-Schlüssel. Siehe Authentifizierung.

Wie der nächtliche Sync funktioniertLink zu diesem Abschnitt#

  • Produkte werden anhand der SKU und Kunden anhand der Kontonummer übereinstimmen, daher ist es harmlos, den Sync zweimal auszuführen.
  • Neue Produkte werden sofort zu ihrem Listenpreis live geschaltet.
  • Geänderte Listenpreise werden nicht live geschaltet. Sie sammeln sich in Entwürfen für Preisanpassungen. Jeden Morgen öffnet jemand Preisanpassungen in der App, prüft die Vorschau und Warnungen, wählt ein Datum aus und veröffentlicht. Kunden können per E-Mail ihre eigenen neuen Preise erhalten.
  • brand, category, manufacturerRef und description werden wie gesendet geschrieben, daher senden Sie immer das vollständige Produkt.
  • Die email und discountBp der Kunden werden ebenfalls wie gesendet geschrieben: null löscht sie.

Senden Sie nur das, was sich geändert hat

Wenn Ihr ERP Ihnen sagen kann, welche Produkte seit dem letzten Lauf geändert wurden, senden Sie nur diese. Weniger Anfragen und weniger Produkte, die in jedem Entwurf gescannt werden müssen.

Nächtlicher Sync (Node.js)Link zu diesem Abschnitt#

Benötigt Node.js 20 oder höher, für die eingebaute fetch. Ersetzen Sie die beiden fromErp-Funktionen durch Lesevorgänge aus Ihrem System.

js
// sync.mjs — run nightly: TRADECATALOG_API_KEY=tc_live_… node sync.mjs
const BASE = 'https://tradecatalog.app/api/v1'
const KEY = process.env.TRADECATALOG_API_KEY
const sleep = (seconds) => new Promise((r) => setTimeout(r, seconds * 1000))

async function api(method, path, body, attempt = 0) {
  const res = await fetch(BASE + path, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
    },
    body: body && JSON.stringify(body),
  })
  if ((res.status === 429 || res.status >= 500) && attempt < 5) {
    await sleep(Number(res.headers.get('Retry-After')) || 2 ** attempt)
    return api(method, path, body, attempt + 1)
  }
  const json = await res.json()
  if (!res.ok) {
    const detail = json.error.issues
      ?.map((i) => `${i.path}: ${i.message}`)
      .join('; ')
    throw new Error(`${method} ${path}: ${json.error.message} ${detail ?? ''}`)
  }
  return json
}

// Replace these with reads from your ERP. Money in pence, discounts in basis points.
async function productsFromErp() {
  return [
    {
      sku: 'HX-8510',
      name: 'Hex bolt M10 x 50 zinc',
      listPrice: 1950,
      unitLabel: 'Box of 100',
      brand: 'Northgate',
      category: 'Bolts',
      availability: 'in_stock',
    },
  ]
}
async function customersFromErp() {
  return [
    {
      accountCode: 'OAK01',
      name: 'Oakfield Joinery',
      email: 'accounts@oakfieldjoinery.co.uk',
      discountBp: 500,
    },
  ]
}

const batches = (rows, size = 100) =>
  Array.from({ length: Math.ceil(rows.length / size) }, (_, i) =>
    rows.slice(i * size, i * size + size),
  )

const { data: workspace } = await api('GET', '/workspace')
console.log(`Syncing into ${workspace.name} (${workspace.currency})`)

for (const customers of batches(await customersFromErp())) {
  const { data } = await api('PUT', '/customers', { customers })
  console.log(`Customers: ${data.created} new, ${data.updated} updated`)
}

// Every batch adds its price changes to the same draft.
let priceChangeId
for (const products of batches(await productsFromErp())) {
  const { data } = await api('PUT', '/products', { products, priceChangeId })
  console.log(`Products: ${data.created} new, ${data.updated} updated`)
  priceChangeId = data.priceChangeId ?? priceChangeId
  if (data.unknownCustomers.length)
    console.warn('Unknown customers in onlyFor:', data.unknownCustomers)
}

if (priceChangeId) {
  const { data } = await api('GET', `/price-changes/${priceChangeId}`)
  console.log(
    `Draft price change: ${data.counts.up} up, ${data.counts.down} down,` +
      ` ${data.warnings.length} warnings. Publish it in TradeCatalog → Price changes.`,
  )
}

Kunden zuerst, damit Produkte mit onlyFor sie finden können.

Führen Sie es aus cron, einer geplanten Aufgabe oder dem Job-Runner Ihres ERP aus:

text
0 2 * * *  cd /opt/tradecatalog && TRADECATALOG_API_KEY=tc_live_… node sync.mjs >> sync.log 2>&1

Bestellpoller (Node.js)Link zu diesem Abschnitt#

Alle paar Minuten neue Bestellungen abrufen, jede an Ihr ERP weiterleiten und als bestätigt markieren. Der Käufer erhält eine E-Mail, dass seine Bestellung bestätigt wurde.

js
// orders.mjs — run every 5 minutes. Copy BASE, KEY, sleep and api() from sync.mjs.
async function sendToErp(order) {
  // Create the sales order in your system. Use order.reference as its external
  // reference, and check it first so a retry can't create it twice.
  console.log(
    `${order.reference} for ${order.customer.accountCode}: ${order.lines.length} lines`,
  )
}

const { data: newOrders } = await api('GET', '/orders?status=new')
for (const summary of newOrders.reverse()) {
  const { data: order } = await api('GET', `/orders/${summary.id}`)
  await sendToErp(order)
  await api('PATCH', `/orders/${order.id}`, { status: 'acknowledged' })
}

Älteste Bestellungen werden zuerst bearbeitet (reverse()), weil die Liste neu zuerst ist.

Wenn der ERP-Schritt erfolgreich ist, aber der PATCH fehlschlägt, wird die Bestellung beim nächsten Mal immer noch new. Deshalb sollte sendToErp vor dem Erstellen von etwas auf order.reference prüfen.

Später, wenn eine Bestellung versandt wird, markieren Sie sie als versandt mit den Versanddetails im Hinweis:

js
await api('PATCH', `/orders/${orderId}`, {
  status: 'dispatched',
  note: 'DPD tracking 1554 2210 0931',
})

Bestellpoller (Python)Link zu diesem Abschnitt#

Derselbe Poller mit requests:

python
# orders.py: run every 5 minutes. TRADECATALOG_API_KEY=tc_live_… python orders.py
import os
import time

import requests

BASE = "https://tradecatalog.app/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TRADECATALOG_API_KEY']}"


def api(method, path, body=None, attempt=0):
    res = session.request(method, BASE + path, json=body, timeout=30)
    if (res.status_code == 429 or res.status_code >= 500) and attempt < 5:
        time.sleep(int(res.headers.get("Retry-After", 2**attempt)))
        return api(method, path, body, attempt + 1)
    data = res.json()
    if not res.ok:
        raise RuntimeError(f"{method} {path}: {data['error']['message']}")
    return data


def send_to_erp(order):
    # Create the sales order in your system; skip it if order["reference"] exists.
    print(order["reference"], order["customer"]["accountCode"], len(order["lines"]), "lines")


for summary in reversed(api("GET", "/orders?status=new")["data"]):
    order = api("GET", f"/orders/{summary['id']}")["data"]
    send_to_erp(order)
    api("PATCH", f"/orders/{order['id']}", {"status": "acknowledged"})

Geld und CodesLink zu diesem Abschnitt#

  • listPrice, unitPrice, lineTotal und total sind in Kleineinheiten (Pence für GBP). Durch 100 teilen, um Pfund zu erhalten.
  • Bestellpreise sind so festgelegt: vor Steuern, wenn Ihr taxLabel „ex …“ sagt (Ihr ERP fügt die Umsatzsteuer beim Rechnen hinzu), einschließlich Steuern, wenn es „inc …“ sagt.
  • Verwenden Sie die Kundenkontonummern Ihres ERP als TradeCatalog-Kontonummern, damit beide Systeme ohne Tabellenabfrage übereinstimmen.

Grenzen für PlanungLink zu diesem Abschnitt#

  • 100 Zeilen pro PUT, und 120 Anfragen pro Minute pro Schlüssel. Ein Katalog mit 10.000 Produkten ist 100 Anfragen, etwa eine Minute.
  • GET /orders deckt die 200 neuesten passenden Bestellungen ab. Pollen Sie häufig genug, dass weniger als 200 neue Bestellungen zwischen den Durchläufen ankommen.
  • Das Veröffentlichen von Preisanpassungen, Rabattregeln und vereinbarten Preisen erfolgt in der App.