Gateway NG keeps products, orders, customers, stock and prices in lockstep between Odoo and any webshop, marketplace, or vendor system โ driven by declarative JSON mappings and a battle-tested transform pipeline. No bespoke integration code per shop.
Not a one-shot importer. A durable, observable sync platform with the guardrails you only notice when they're missing.
Read and write both ways between Odoo and any shop. Products, orders, customers, stock, prices, categories โ round-tripped without data loss.
Field-by-field mappings live as versioned JSON with a typed schema. Override per tenant in the DB โ no redeploy to change how a field maps.
The battle-tested transform engine from srmigration: compose, default, and lambda-transform source fields into target records with full type safety.
Per-model watermarks track the last-seen timestamp so each run moves only what changed. Delete a watermark to trigger a full backfill.
fnmatch-style globs decide who wins when both sides change a field. First match wins, identity keys are always preserved โ no silent clobbering.
APScheduler fires syncs on a cron schedule with stuck-job detection; an arq-backed queue runs them async so big catalogs never block the API.
A React console to browse every connector's field tables, edit mappings, trigger and watch runs live, and inspect the raw per-record changelog.
Tenant-scoped connectors, connections and mappings with a three-level role resolver (user โบ workspace โบ company) and per-tenant rate limits.
OIDC Authorization-Code + PKCE against Lastloop. JWTs are validated server-side, every endpoint is auth-gated, and runs are fully audited.
Each connector ships a collection of JSON model mappings. The engine reads the source, runs every field through the MAPTO pipeline into typed Pydantic records, diffs against the target, applies conflict rules, and writes only what changed โ the same path for every shop.
// Shopify customer โ Odoo res.partner โ declarative, versioned { "model": "res.partner", "source_table": "shopify_customer", "source_id_field": "id", "record": { "name": { "MAPTO": ["firstName", "lastName", "lambda f, l: f'{f} {l}'.strip()"], "type": "char", "required": true }, "email": { "MAPTO": "email", "type": "char" }, "city": { "MAPTO": ["defaultAddress", "lambda a: a.get('city','') if a else ''"], "type": "char" }, "customer_rank": { "MAPVALUE": 1, "type": "integer" } } }
Every connector speaks the same internal contract, so adding a platform is a mapping + an API client โ never a rewrite of the engine.
| Platform | Direction | Status | Notes |
|---|---|---|---|
| Odoo (JSON-RPC / XML-RPC) | Bidirectional | Production | Incremental watermarks |
| Shopify (Admin API) | Bidirectional | Production | REST + GraphQL bulk ops |
| WooCommerce (REST) | Bidirectional | Production | REST v3 |
| Magento 2 (REST) | Bidirectional | Production | Catalog + stock |
| OpenCart (REST) | Bidirectional | Beta | Code shipped, tests pending |
| Shopware 6 (REST) | Bidirectional | Beta | Code shipped, tests pending |
| CSV / Excel | Bidirectional | Designed | MinIO / S3 codecs |
Connect a shop, pick a mapping, hit run, and watch records round-trip live. The console hides nothing.