The first version of every RFID-to-Odoo integration looks the same. A reader is configured to POST each tag read to a controller, the controller calls write on stock.quant, and within a week the database is locking, the Odoo workers are saturated, and the inventory is wrong in a way nobody can reproduce.
The mistake is architectural rather than technical. A reader is a sensor, not a transaction source. Between a sensor that reports thousands of times per second and an ERP that expects a considered business event, there has to be something that thinks. That something is the gateway, and in a working deployment it is the part you maintain, version and monitor — the product, not the glue.
Why wiring readers straight into Odoo is an anti-pattern
The numbers make the case before any code is written.
- A single fixed reader in a doorway emits several hundred TagRead events per second during a pallet pass.
- Each tag is typically seen five to twenty times inside one second, at different signal strengths.
- Odoo's ORM computes and writes one quant row per product, location and lot, with row locks and cache invalidation on each write.
- Concurrent writers on the same quant serialise into lock waits, and multi-row updates in the wrong order produce deadlocks.
- A reader outage then replays a burst, and the burst arrives at exactly the moment the ERP is least able to absorb it.
The gateway's three jobs: parse, deduplicate, aggregate
A gateway that does these three things well can be two hundred lines. One that does them badly will be rewritten twice.
- Parse: turn a raw EPC into a product, a lot and a serial without a database lookup on the hot path.
- Deduplicate: keep one observation per tag per time window, not twenty, and keep the strongest signal with it.
- Aggregate: convert observations into signed deltas per location and product, which is the only quantity Odoo should ever see.
- Buffer: hold deltas in a durable spool so a restart loses nothing and a retry is safe.
- Apply: write to Odoo in batches, with an idempotency key that makes a repeated apply a no-op.
From EPC to product: a deterministic mapping
EPC parsing is where integrations quietly break, because it is done by string manipulation instead of by schema.
- A 96-bit SGTIN encodes a company prefix, an item reference and a serial; the split point depends on the prefix length, not on the string.
- Decode the header first, then the partition, then the fields; guessing field widths works until it does not.
- Keep a cached map from item reference to product and lot; refresh it on a timer rather than on every read.
- Treat unknown EPCs as an explicit queue, not as nulls; an unknown tag is a labelling or commissioning defect worth seeing.
- Version the mapping rules, because a supplier changing its tag encoding should not require a code deployment.
Writing stock.quant without deadlocks
The write path is where most projects lose a weekend. The rules are simple and mostly about ordering.
- Use one writer process per database; concurrency here buys throughput you do not need and deadlocks you do.
- Prefer the ORM method that performs an atomic quantity update with the correct lock, and never read-modify-write in Python.
- If you must use SQL, lock rows in a deterministic order — product, then location, then lot — so two transactions cannot cross.
- Batch by location, not by tag: fifty deltas in one aisle belong in one transaction, not fifty.
- Record the business reason on every adjustment, because an unexplained quantity change is indistinguishable from a bug during an audit.
Conflict resolution when two truths disagree
The reader and the ERP will disagree, and the design question is which one is allowed to win, and when.
- A read is evidence about the present; an ERP record is a claim about the past. They are not the same kind of fact.
- Never let a single read overwrite a quantity; apply a delta only when the observation is complete and the zone is closed.
- Use event time from the reader, not arrival time at the gateway, so a delayed batch does not look like a new movement.
- Last-write-wins is unsafe with duplicated events; prefer a monotonic sequence per reader and reject anything older than the last applied.
- When the disagreement is structural — a whole zone off by ten percent — raise an exception instead of silently correcting.
Throughput engineering at 100,000 events a day
A hundred thousand events is one per second on average and two hundred per second at the dock door, so averages are useless.
- Size for the peak, then verify the average; the peak is what breaks the system and the average is what makes it look fine in a demo.
- Batch on a half-second to two-second window; below that you pay transaction overhead, above it you pay latency.
- Keep the hot path free of ORM computed fields, onchange logic and mail threading; none of them belong in an inventory write.
- Run the apply worker under the job queue with a dedicated channel, so a slow ERP does not stall the reader-facing side.
- Measure the dedupe ratio; if it is below ten to one, the time window is too short and Odoo is doing unnecessary work.
Observability: what to log and what to alert
An inventory pipeline that cannot be observed will be trusted until the first audit, and then not at all.
- Log every raw event for a sampled subset, and every applied delta for all events, with the idempotency key attached.
- Track per-antenna read counts, so a dead antenna looks like a drop in a series rather than a quiet week.
- Alert on apply latency above a threshold, on spool depth growth, and on a rising rate of rejected events.
- Reconcile daily: compare gateway deltas against Odoo quantity changes and explain every gap larger than a threshold.
- Keep a replayable event archive, because the most common incident is not a crash but a wrong rule applied for three days.
Deployment sequence and rollback
The safe way to introduce a write path into an ERP is to make it earn the right to write.
- Stage one: shadow mode. Compute deltas, log them, write nothing, and compare against expected results for two weeks.
- Stage two: write to a sandbox database with production traffic, and run the reconciliation report against it.
- Stage three: enable writes for one zone with a feature flag, and keep the old process running in parallel.
- Stage four: widen by zone, watching the exception rate rather than the success rate.
- Rollback is not a database restore: stop the writer, replay the spool into a corrected rule, and reapply idempotently.






