Erpy for Craft CMS

Erpy for Odoo

Erpy for Odoo connects Craft Commerce to Odoo, Community or Enterprise, self-hosted or Odoo Online, through Odoo's external JSON-RPC API. The add-on is free; it needs Erpy, which is the paid part and owns the sync engine, the identity map, mapping, the queue and the log.

Install

composer require justinholtweb/craft-erpy-odoo
php craft plugin/install erpy-odoo

Then Erpy → Connections → New connection and pick Odoo. The add-on has no settings screen of its own; everything lives on the connection.

What it syncs

EntityDirectionDelta syncPage size
CustomersERP → CommerceYes, on write_date200
ProductsERP → CommerceYes, on write_date200
PricesERP → CommerceYes, on write_date200
InventoryERP → CommerceYes, on write_date500
OrdersCommerce → ERP
Order statusERP → CommerceYes200
ShipmentsERP → CommerceYes200
InvoicesERP → CommerceYes200
CreditERP → CommerceNo200

write_date is indexed on every Odoo model, which makes delta syncing cheap and reliable here in a way it is not on several of the older ERPs. The connector declares multi-company support and that Odoo offers a sandbox: a staging database or a trial instance is a good first target.

Connecting

FieldWhat to put there
Odoo URLe.g. https://example.odoo.com.
DatabaseOn Odoo Online this is usually the subdomain.
LoginThe email address of a dedicated integration user.
API keyGenerated under that user's Preferences → Account Security.
Warehouse IDThe numeric id of the stock location stock is read from, including its child locations. Leave blank for all internal locations.
Base pricelist IDItems on this pricelist are left out of the contract price sync.
Sales team IDOptional. Web orders are often given their own team so they can be reported on separately.
Confirm orders on arrivalOff by default: orders arrive as quotations for somebody to review. On confirms them, which reserves stock straight away.

Use an API key, not the password. An API key works everywhere a password does and can be revoked on its own. Some Odoo Online instances refuse password logins over RPC entirely.

If Test connection says Odoo would not accept the credentials, check the database name as well as the key. Odoo answers a wrong database exactly as it answers a wrong password. A passing test shows the server version and how many sellable products the user can see.

Things to know about Odoo

It is not a REST API. There are no endpoints, only models and methods. Everything the connector reads is a search_read against a model with a domain, and an order is a create on sale.order.

Errors arrive as HTTP 200. A missing model, a permission failure and a Python traceback all come back with a success status and an error object in the body. So the connector reads the body, not the status. A failed login is false with a 200, too. The log shows the real error.

The SKU is default_code. Odoo's Internal Reference is the only field that behaves like a SKU. A product without one arrives with an empty SKU and cannot be matched to Commerce. Only products marked Can be sold are read, and only storable products have their stock tracked.

Relations are [id, "Display Name"]. A many-to-one field is a two-element array, not a scalar. The connector unpacks them; a mapping rule reading one from raw. gets the array. Use raw.categ_id.1 for the name and raw.categ_id.0 for the id.

Stock is what is not already reserved. Inventory reads stock.quant in internal locations and uses Odoo's available_quantity, not the on-hand figure.

Only two kinds of pricelist rule can be honoured. A pricelist item is a fixed price, a percentage off, or a formula. The first two come through as contract prices. A formula cannot be honoured without re-implementing Odoo's pricing engine, and it does not come through correctly. Check contract prices from formula-based pricelists on the mapping preview before switching contract pricing on.

Odoo stores 0 for "no credit limit". That is not the same as a limit of zero, and it is not read as one.

Times are UTC with no marker. Odoo stores write_date in UTC without saying so. The connector converts the watermark before comparing, because formatting it as-is would make a site in a negative offset skip several hours of changes on every sync.

Customer codes are ref. A partner's Reference is its customer code. A partner with no reference falls back to its numeric id, so it can still be addressed. When an order is pushed, the customer is found by linked id, then by reference, then by email address. Give your B2B partners a reference.

Your order number is client_order_ref. The Commerce order number goes out as the quotation's customer reference. A retried job asks for it before creating, so it does not leave two quotations behind. Order status and shipments come back matched on it. Every line's SKU must match a product's Internal Reference, or the order is refused and names the SKU.

Correcting a field

If your catalogue keys on the barcode rather than the Internal Reference, correct the SKU on the Products mapping. barcode is one of the fields the product read requests, so it is in the raw payload:

raw.barcode   →   sku        transform: nullif: | trim

The nullif:, with nothing after the colon, is there because Odoo sends an empty field as false, not as an empty string. It turns that false into nothing, and a rule that resolves to nothing is skipped. So a product with no barcode keeps its Internal Reference as its SKU rather than losing it.

See Field mapping for the rule syntax and the full transform list, Syncing for runs and watermarks, and Troubleshooting when something does not arrive.