Skip to main content

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.

CI License: MIT Python 3.11+

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 with REPLICATION, 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.

  1. 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
    
  2. Follow erpdoo's log: docker compose logs -f erpdoo.
  3. Open Odoo at http://localhost:8069 (login admin, password admin), go to Sales, open a quotation and click Confirm.
  4. The SaleOrderConfirmed event appears in the log, and in Redpanda Console at http://localhost:8080: topic erpdoo.events has it as JSON Schema, erpdoo.events.avro in Avro, and Schema Registry lists each event type's schema. Posting a customer invoice gives an InvoicePosted event the same way.
  5. 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:

  1. Have a database admin prepare Postgres once, as described in the Postgres setup guide.
  2. 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
    
  3. 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, set viewer: { host: 0.0.0.0 } in erpdoo.yml and 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
      
  4. 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

MIT

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)

Source distribution for erpdoo 0.1.0
File Size Uploaded Provenance
erpdoo-0.1.0.tar.gz 170.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for erpdoo 0.1.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page