erpdoo
An Odoo Kafka connector and webhooks: change data capture that turns Odoo's database changes into clean business events, with nothing installed inside Odoo.
What it does
erpdoo reads Odoo's PostgreSQL logical replication stream directly, with no Debezium and no Kafka Connect, and turns each committed transaction into business events such as SaleOrderConfirmed or InvoicePaid, with the customer, order lines and products filled in.
Nothing is installed inside Odoo, and no code is needed per event: each event is a short YAML rule, and six ship with erpdoo. Events go to Kafka (JSON Schema or Avro, with schemas registered automatically), a webhook or stdout, and messages from other systems can act on Odoo in the other direction.
Version 0.1: tested end to end; configuration may change before 1.0.
Odoo ──▶ PostgreSQL ──(logical replication)──▶ erpdoo ──▶ Kafka / Redpanda / webhook / stdout
▲ │
└───────────(Odoo's JSON-RPC API)──────────────┴◀── Kafka (inbound messages)
Features
- No code per event. YAML rules on state transitions, field changes, creates and deletes; packs for sales, invoicing, inventory and CRM ship with erpdoo (events, rules).
- Complete events. A transaction's changes are grouped and evaluated at commit, and related records come from a local state store, without querying Odoo (architecture).
- Exactly once to Kafka, with Kafka transactions; at-least-once to webhooks, with deterministic event ids to dedupe on (delivery).
- Schemas registered automatically, one subject per event type in JSON Schema or Avro, with breaking rule changes refused at startup (schemas).
- Backfill of past events from Odoo's own field tracking, by itself after a lost replication slot or a late app install (backfill).
- Inbound rules: messages from a warehouse or payment provider act on Odoo through its API, exactly once and without event loops (inbound).
- Live viewer that shows each transaction's raw changes next to the events built from it (viewer).
- Fast: 18.4 ms p50 from Postgres commit to Kafka consumer, and 2,500 to 3,000 events a second through a backlog, on one laptop (performance).
Requirements
- Odoo 18 Community, on PostgreSQL 16: what erpdoo is developed and tested against.
- Python 3.11 or later, or Docker.
- A sink: Kafka or Redpanda (a schema registry is optional), or a webhook endpoint.
- Replication access to Odoo's Postgres:
wal_level=logical, a role withREPLICATION, and a database admin who runs a few SQL statements once (Postgres setup). erpdoo's role stays read-only. - Or, for hosted Odoo without replication access: polling mode reads Odoo's API instead, with real limits.
Quick start
The dev stack runs Odoo 18 with demo data, Postgres 16, Redpanda with its schema registry, Redpanda Console and erpdoo. It needs Docker with Compose v2.
- Start it. The first start downloads about 2 GB of images, then installs Odoo with demo data (about a minute).
git clone https://github.com/theicelotus/erpdoo && cd erpdoo docker compose up -d --build
- Follow erpdoo's log:
docker compose logs -f erpdoo. - Open Odoo at http://localhost:8069 (login
admin, passwordadmin), go to Sales, open a quotation and click Confirm. - The
SaleOrderConfirmedevent appears in the log, and in Redpanda Console at http://localhost:8080: topicerpdoo.eventshas it as JSON Schema,erpdoo.events.avroin Avro, and Schema Registry lists each event type's schema. Posting a customer invoice gives anInvoicePostedevent the same way. - Open the live viewer at http://localhost:8000 to see each transaction's raw changes next to its events.
A full example event is in docs/events.md.
Installation
For your own self-hosted Odoo:
- Have a database admin prepare Postgres once, as described in the Postgres setup guide.
- Tell erpdoo where events go, in
erpdoo.yml:sinks: - type: kafka bootstrap_servers: ${KAFKA_BOOTSTRAP:-kafka:9092} topic: erpdoo.events schema_registry: url: http://schema-registry:8081
- Install and run erpdoo in one of these ways:
- With pip (Python 3.11 or later), with the live viewer's extra. Copy the shipped rules from rules/ into the directory with
erpdoo.yml, run erpdoo there, and set where Odoo's Postgres is and where the state store's SQLite file goes (on persistent storage):pip install 'erpdoo[viewer]' export ERPDOO_PG_DSN='host=db dbname=odoo user=erpdoo password=...' ERPDOO_STATE=/var/lib/erpdoo/state.db erpdoo
- With Docker, from the published image, which has the viewer and the shipped rules. Mount your
erpdoo.yml, and a volume for the state store:docker run -d --name erpdoo --restart unless-stopped \ -e ERPDOO_PG_DSN='host=db dbname=odoo user=erpdoo password=...' \ -v "$PWD/erpdoo.yml:/app/erpdoo.yml:ro" -v erpdoo-state:/var/lib/erpdoo \ ghcr.io/theicelotus/erpdoo:0.1.0
To use your own rules, mount them at/app/rules. For the live viewer, setviewer: { host: 0.0.0.0 }inerpdoo.ymland add-p 127.0.0.1:8000:8000. - From source, from a clone, with pip or by building the image:
git clone https://github.com/theicelotus/erpdoo pip install './erpdoo[viewer]' # or: docker build -t erpdoo ./erpdoo
- With pip (Python 3.11 or later), with the live viewer's extra. Copy the shipped rules from rules/ into the directory with
- Run it under a supervisor (systemd, or Docker's restart policy). When Postgres or a sink goes away it exits without confirming what is undelivered, and continues from there when restarted.
All settings, ERPDOO_RULES and ERPDOO_CONFIG among them, are in Configuration.
Usage
A rule is a model, a condition and the related records to include. This is the shipped SaleOrderConfirmed, shortened (rules/sales.yml):
SaleOrderConfirmed:
model: sale.order
when:
state: { to: sale } # changed to sale in this transaction
include:
partner_id: [name, email, phone, vat, is_company]
order_line: [name, product_uom_qty, price_unit, price_subtotal]
order_line.product_id: [name, default_code]
A consumer on the dev stack, with confluent-kafka (pip install 'confluent-kafka[json]'), whose deserializer fetches each message's schema from the registry and checks the event against it:
from confluent_kafka import Consumer
from confluent_kafka.schema_registry import SchemaRegistryClient
from confluent_kafka.schema_registry.json_schema import JSONDeserializer
from confluent_kafka.serialization import MessageField, SerializationContext
registry = SchemaRegistryClient({"url": "http://127.0.0.1:18081"})
deserialize = JSONDeserializer(None, schema_registry_client=registry) # each message's own schema
consumer = Consumer({"bootstrap.servers": "127.0.0.1:19092", "group.id": "billing",
"auto.offset.reset": "earliest"}) # read_committed is librdkafka's default
consumer.subscribe(["erpdoo.events"])
while True:
msg = consumer.poll(1.0)
if msg is not None and not msg.error():
event = deserialize(msg.value(), SerializationContext(msg.topic(), MessageField.VALUE))
print(event["event_id"], event["event_type"], event["record"]["name"])
Commands:
erpdoo # stream events, by the rules and erpdoo.yml
erpdoo backfill --since 2026-10-01T00:00:00Z --until 2026-10-02T00:00:00Z [--dry-run]
python -m erpdoo.models sale.order # how an Odoo model maps to its table
docker compose restart erpdoo # dev stack: after editing rules/ or erpdoo.yml
Documentation
- Architecture: how erpdoo works, the state store and the live viewer
- Events: the shipped events and the event format
- Writing rules: the YAML rule format
- Sinks and delivery: Kafka, webhook, stdout, dead-letter topics, exactly once
- Schemas: JSON Schema and Avro, field types, compatibility, consumers
- Inbound: messages that act on Odoo, the integration user, loop safety
- Polling mode: hosted Odoo through its API, and its limits
- Backfill: rebuilding past events, apps installed later
- Setting up Postgres: the role, publication and grants
- Slot health: replication lag, WAL limits, invalidated slots
- Performance: latency, throughput, state store size, WAL cost
- Configuration: environment variables and erpdoo.yml
- Changelog: what changed in each release
Contributing
Bug reports and pull requests are welcome. CONTRIBUTING.md covers the dev stack and the checks every change must pass.
License
Metadata
Release files for erpdoo 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| erpdoo-0.1.0.tar.gz | 170.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| erpdoo-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 283.2 kB
Release files / erpdoo-0.1.0.tar.gz
| Download URL | erpdoo-0.1.0.tar.gz |
|---|---|
| Size | 170.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c4439d12a4449e47bbc56b7e3b96e95a94aab70f07627f712fd75228f3f5e196
|
|
BLAKE2b-256 checksum How to use checksums |
e35ae03b6d9e600048d81c7576dc9d8a4bae1344f262fbfc64215ac227a874cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / erpdoo-0.1.0-py3-none-any.whl
| Download URL | erpdoo-0.1.0-py3-none-any.whl |
|---|---|
| Size | 113.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eafd5ab90762e6f3fe4e5be13532573a239e4bdd0cfe9cafbd752cc8e2897caa
|
|
BLAKE2b-256 checksum How to use checksums |
8f07d07e462eeb2883ce25a21b26b6d5fbf403bc1452b6e0e815458ee27daab1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for erpdoo 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for erpdoo-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.