Ce guide met en œuvre les deux tâches dont les fournisseurs ont le plus besoin :
- Synchronisation nocturne : envoyer les produits et les clients depuis votre logiciel de gestion de production (ERP) vers TradeCatalog.
- Poller de commandes : toutes les quelques minutes, récupérer les nouvelles commandes dans votre ERP et les marquer comme reçues.
Vous avez besoin d'une clé d'API Lecture et écriture. Voir Authentification.
Comment se comporte la synchronisation nocturneLien vers cette section#
- Les produits sont associés par référence (SKU) et les clients par code de compte, donc exécuter la synchronisation deux fois est inoffensif.
- Les nouveaux produits deviennent visibles à leur prix de liste immédiatement.
- Les prix de liste modifiés ne deviennent pas visibles. Ils sont stockés dans des changements de prix en brouillon. Chaque matin, quelqu'un ouvre Changements de prix dans l'application, vérifie l'aperçu et les avertissements, sélectionne une date et publie. Les clients peuvent recevoir un courriel avec leurs nouveaux prix.
brand,category,manufacturerRefetdescriptionsont écrits comme envoyés, donc envoyez toujours le produit complet.- Les
emailetdiscountBpdes clients sont également écrits comme envoyés :nullles efface.
Envoyez uniquement ce qui a changé
Si votre ERP peut vous indiquer quels produits ont changé depuis la dernière exécution, envoyez uniquement ceux-ci. Moins de demandes, et moins de produits à scanner à chaque brouillon.
Synchronisation nocturne (Node.js)Lien vers cette section#
Nécessite Node.js 20 ou version ultérieure, pour le fetch intégré. Remplacez les deux fonctions fromErp par des lectures de votre système.
// 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.`,
)
}
Les clients sont traités en premier, donc les produits avec onlyFor peuvent les trouver.
Exécutez-le depuis cron, une tâche planifiée ou le gestionnaire de tâches de votre ERP :
0 2 * * * cd /opt/tradecatalog && TRADECATALOG_API_KEY=tc_live_… node sync.mjs >> sync.log 2>&1
Poller de commandes (Node.js)Lien vers cette section#
Toutes les quelques minutes, récupérez les nouvelles commandes, transmettez-les à votre ERP, puis marquez-les comme reçues. L'acheteur reçoit un courriel indiquant que sa commande a été reçue.
// 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' })
}
Les commandes les plus anciennes sont traitées en premier (reverse()), car la liste est triée par date de création.
Si l'étape de votre ERP réussit mais que le PATCH échoue, la commande est toujours new lors de la prochaine exécution. C'est pourquoi sendToErp doit vérifier les order.reference avant de créer quoi que ce soit.
Plus tard, lorsqu'une commande est expédiée, marquez-la comme expédiée avec les détails de suivi dans la note :
await api('PATCH', `/orders/${orderId}`, {
status: 'dispatched',
note: 'DPD tracking 1554 2210 0931',
})
Poller de commandes (Python)Lien vers cette section#
Le même poller avec requests :
# 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"})
Argent et codesLien vers cette section#
listPrice,unitPrice,lineTotalettotalsont en unités mineures (pence pour le GBP). Divisez par 100 pour obtenir les livres.- Les prix des commandes sont tels que vous les avez définis : avant taxe si votre
taxLabelindique « ex … » (votre ERP ajoute la TVA lors de la facturation), incluant la taxe si cela indique « inc … ». - Utilisez les codes de compte clients de votre ERP comme codes de compte TradeCatalog, afin que les deux systèmes correspondent sans table de correspondance.
Limites à prévoirLien vers cette section#
- 100 lignes par
PUT, et 120 requêtes par minute par clé. Un catalogue de 10 000 produits est de 100 requêtes, environ une minute. GET /orderscouvre les 200 commandes les plus récentes correspondantes. Faites le poll souvent suffisamment pour que moins de 200 nouvelles commandes arrivent entre deux exécutions.- La publication des changements de prix, des règles de remise et des prix convenus se fait dans l'application.