Naar inhoud
Documentatie menu

Referentie

Handleidingen

Synchroniseer met je ERP

Een volledige nachtelijke product- en klantensynchronisatie, en een bestellingspoller die nieuwe bestellingen naar uw eigen systeem haalt.

Deze gids bouwt de twee taken die de meeste leveranciers nodig hebben:

  1. Nachtsynchronisatie: producten en klanten van je ERP naar TradeCatalog pushen.
  2. Bestellingspoller: elke paar minuten nieuwe bestellingen halen naar je ERP en ze markeren als aangekomen.

Je hebt een Lees- en schrijf API-sleutel nodig. Zie Authenticatie.

Hoe de nachtsynchronisatie zich gedraagtLink naar deze sectie#

  • Producten worden geïdentificeerd aan de hand van SKU en klanten aan de hand van accountcode, dus het uitvoeren van de synchronisatie twee keer is onschadelijk.
  • Nieuwe producten worden direct op hun lijstprijs live gebracht.
  • Aanpassingen aan lijstprijzen gaan niet direct live. Ze worden opgeslagen als conceptprijsveranderingen. Elke ochtend moet iemand Prijsveranderingen in de app openen, de voorbeeldweergave en waarschuwingen controleren, een datum kiezen en publiceren. Klanten kunnen hun eigen nieuwe prijzen worden geëmaild.
  • brand, category, manufacturerRef en description worden zoals verstuurd geschreven, dus stuur altijd het volledige product.
  • De email en discountBp van klanten worden ook zoals verstuurd geschreven: null maakt ze leeg.

Stuur alleen wat is gewijzigd

Als je ERP je kan vertellen welke producten sinds de laatste uitvoering zijn gewijzigd, stuur dan alleen die. Minder aanvragen, en minder producten om te scannen in elk concept.

Nachtsynchronisatie (Node.js)Link naar deze sectie#

Vereist Node.js 20 of nieuwer, voor de ingebouwde fetch. Vervang de twee fromErp functies met lezingen uit je systeem.

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.`,
  )
}

Klanten gaan eerst, zodat producten met onlyFor ze kunnen vinden.

Voer het uit vanaf cron, een geplande taak of de job runner van je ERP:

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

Bestellingspoller (Node.js)Link naar deze sectie#

Elke paar minuten, nieuwe bestellingen ophalen, elk aan je ERP doorgeven en deze als aangekomen markeren. De koper krijgt een e-mail dat hun bestelling is aangekomen.

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' })
}

Oudste bestellingen worden eerst behandeld (reverse()), omdat de lijst nieuwste eerst is.

Als de ERP-stap slaagt, maar de PATCH mislukt, is de bestelling nog steeds new de volgende keer. Daarom moet sendToErp controleren op order.reference voordat iets wordt gemaakt.

Later, wanneer een bestelling wordt verzonden, markeer deze als verzonden met de trackingdetails in de opmerking:

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

Bestellingspoller (Python)Link naar deze sectie#

Deze dezelfde poller met 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 en codesLink naar deze sectie#

  • listPrice, unitPrice, lineTotal en total zijn in mindere eenheden (pence voor GBP). Deel door 100 voor ponden.
  • Bestellingsprijzen zijn zoals je ze instelt: voor belasting als je taxLabel zegt “ex …” (je ERP voegt BTW toe wanneer het factureert), inclusief belasting als het zegt “inc …”.
  • Gebruik je ERP’s klantaccountcodes als TradeCatalog accountcodes, zodat beide systemen zonder lookup-tabel overeenkomen.

Beperkingen om rekening mee te houdenLink naar deze sectie#

  • 100 rijen per PUT, en 120 aanvragen per minuut per sleutel. Een catalogus van 10.000 producten is 100 aanvragen, ongeveer een minuut.
  • GET /orders dekt de 200 meest recente overeenkomende bestellingen. Poll voldoende vaak dat minder dan 200 nieuwe bestellingen tussen de uitvoeringen arriveren.
  • Het publiceren van prijsveranderingen, kortingregels en overeengekomen prijzen wordt gedaan in de app.