We built SideQuest for Odoo. Here's the honest state. Beta
For the last several days the bulk of our build work has been a sister product: SideQuest for Odoo. The same five-step matcher, the same draft-for-review gate, the same local-first data posture as the QuickBooks Online connector — but talking XML-RPC to a sale.order instead of REST to a QuickBooks Estimate. Phases 1 through 4 are built and validated live against an Odoo 17 sandbox using a real Gmail inbox. This post is a straight accounting of what works, what doesn't, and the architecture calls that mattered.
[email protected] with subject "Odoo beta interest" to join the next batch.
Why Odoo
The QBO connector solved a specific shape of problem: a distributor on QuickBooks Online getting POs by email, with someone retyping every line into Estimates. That same shape sits on top of Odoo. Different ERP, same painful workflow, different ecosystem of failed fixes around it (Conexiom, Rossum, Odoo dev shops). Distributors on Odoo asked us if we'd come over. The answer was yes if the architecture could be reused without forking.
The architecture reused cleanly. The matcher, parser, draft store, cross-reference flywheel, insights engines, and dashboard renderer are pure data-in/data-out modules with no QuickBooks coupling. Odoo just needed a different client at the bottom of the stack — a OdooClient instead of a QBClient — speaking XML-RPC with a per-user API key, returning the same shape of records.
What's live
Seven phases are validated against a live Odoo 17 sandbox running in Docker, with a real Gmail token minted via OAuth. 66 unit tests green, five live integration smoke suites green (the most recent: Phase 7 operator-productivity tools live-tested against real drafts — review-queue surfaced our dirty $283K Contractor PO from earlier testing, price-variance correctly flagged a po_higher line, and the bulk-submit safety contract held):
- Phase 1 — Reads.
list_items,list_customers,list_all_open_invoices. Products come back in the same shape the matcher expects. Customers carry order_count, BillAddr, ShipAddr, PrimaryEmailAddr, CreditLimit, with awkward-case keys correctly omitted (no email → no key in the dict). - Phase 2 — Quotation writes.
create_draft_quotation(writes asale.orderinstate='draft', idempotent onclient_order_ref),add_line,update_line,remove_line,confirm_quotation(draft → sent),discard_quotation. Tax is computed by Odoo server-side and read back — no client-side tax math. Non-draft mutations get rejected with a typed skip envelope, not an exception. - Phase 3 — Propose-to-submit.
propose_estimateparses a PO (text or PDF), runs the five-step matcher against the live Odoo catalog, stages a local draft for review, and refuses to submit any line with no matched product. The submit path creates the Quotation in Odoo asstate='draft'. The local draft flips to submitted. Operator-submit contract holds. - Phase 4 — Gmail + insights + dashboard.
list_incoming_pos,parse_po_from_email,mark_email_processedover the real Gmail API atgmail.modifyscope (read and draft, never send).audit_catalog,pricing_intelligence,customer_healthreused verbatim from the shared core. Multi-period HTML dashboard with KPI row, throughput, match quality, top items/customers, time saved, catalog hygiene, customer health, and pricing intelligence — all populated from real Odoo data plus the local drafts store, all rendered offline. - Cross-reference flywheel. When an operator assigns a product to an unknown PO line and submits, the connector writes a row to
cross_reference.csvmapping(customer_id, customer_part) → internal_sku. Next session, the same part from that customer auto-matches at confidence 1.0. The matcher gets smarter every time anyone uses it. This is the compounding moat the QBO product runs on, ported intact. - Phase 5 — Doctor self-check CLI. Twelve PASS/FAIL/WARN checks across the operator's install: .env present and complete, Odoo reachable, authenticated, sale_management + stock modules installed, catalog has products, Gmail OAuth client secret + token + label, drafts store writable, cross-reference store readable, license configured, and a version stamp. One command (
sidequest-odoo doctor) gives the operator a colored report and an exit code for scripts. Mirrors the QBO connector'sdiagnosetool one-for-one. - Phase 6 — Usage reporting to production control plane. The connector records
po_processedandestimate_submittedevents to a local SQLite log, then flushes them to the control plane on boot and every 30 minutes thereafter via a background daemon thread. Split skip reasons (control_plane_disabledvsno_license_key) with per-reason fix hints, so an operator can tell exactly which gate fired. Server-side, every Odoo license carriesproduct='odoo'and events land in a separate admin view, so beta-test traffic never contaminates the QBO metrics our paying customers drive. - Phase 7 — Operator productivity tools. Five MCP tools that close the QBO parity gap on the operator workflow:
report_review_queue(every draft that didn't pass the clean-gate, with reasons),check_price_variance(flags PO lines outside the configured tolerance vs the Odoo catalog),find_outlier_lines(scans drafts + recent Quotations for absurd quantities / prices),bulk_submit_clean(submits every clean-gate-passing draft in one call, hard-gated by theSIDEQUEST_AUTOSUBMITenv so it can never fire by accident), andbulk_discard_drafts(local-only cleanup with dry-run default + AND-composed filters). The shared_evaluate_clean_gatehelper is vendored from the QBO connector and serves both the review-queue report and the bulk-submit-clean gate, ensuring "clean" means the same thing across the operator's view and the auto-submit decision.
The four architecture calls that mattered most
1. Vendor, don't extract — at least until the gate is open
The shared core lives in the QBO connector as canonical and is copied verbatim into the Odoo project, with a drift ledger and a discipline rule: any change to a shared file in QBO has to be re-vendored to Odoo at the same version, or the divergence is logged and explained. The alternative was extracting a sidequest_core package both products import. We chose vendoring because pre-launch, the cost of accidental breakage in QBO (which has paying customers) outweighs the cost of duplication. Extraction is queued for after Odoo exits beta.
2. Local-first stays local-first
The privacy posture from QBO carries over without compromise. PO contents, customer pricing, the Odoo catalog, and the learned cross-references all live on the operator's machine. The only thing reported back to our control plane is an anonymous monthly PO count for usage metering. As of Phase 6 that wiring is live — but the control plane runs Odoo licenses on a separate product tier from QBO, so beta-test traffic shows up in its own admin view and never contaminates the QBO numbers our paying customers drive.
3. Build the XML-RPC client inside try blocks
Odoo's XML-RPC surface throws xmlrpc.client.Fault with a 30-line Python traceback embedded in the fault string. The first version of OdooClient let those propagate. The second wraps every call in a try block, catches the Fault, parses the underlying error class out of the traceback, and emits the same typed error envelope the QBO connector uses: {"error": "odoo_unavailable", "exception_type": "OdooAuthFailed", "message": "...", "hint": "..."}. The CLI and MCP tools never see a raw Python exception. The diagnose prompt linking to /diagnose works the same way it does for QBO.
4. Don't trust Odoo's button methods over XML-RPC
Phase 2's first live test failed in an instructive way: action_quotation_sent() raised a Fault over XML-RPC even though the state change committed. Odoo's button methods fire mail-tracking side effects and return non-serializable action dicts that don't marshal cleanly through XML-RPC. The fix was to set state via a plain write({'state': 'sent'}) — deterministic, side-effect-free, exactly what a programmatic submit wants. We do not call action_confirm() or action_cancel() either. Both gotchas are documented in the project's FINDINGS log so the next person doesn't relearn them.
What's still beta
Honestly: a lot. The connector works end-to-end, but the operator polish that makes it feel like a finished product still has gaps:
- Twelve parity reporting tools as queryable Claude Desktop commands (top customers, top items, time saved, match quality by customer, POs processed in period, etc.). The data already renders in the offline dashboard; the per-tool Claude Desktop queries are the gap.
- Auto-submit-if-clean for high-confidence, named-customer POs (will land as opt-in per customer, hard-gated by the same SIDEQUEST_AUTOSUBMIT env flag the bulk-submit safety contract uses).
- Auto-label-unprocessed for Gmail batch labeling.
- AR-followup sweep (QBO has it; Odoo equivalent queued).
- Multi-attachment routing — choosing the right PDF out of an email with three attached docs.
- OCR rescue (Azure Document Intelligence) for the hardest scans — handwritten POs, fax-quality output.
- An external tester running it on their own Odoo instance, end-to-end, in their own real workflow.
The release gate exists to keep us honest about that last point. No paying customer touches this until an external tester signs off and we sign off. We're not skipping that.
What we're optimizing for
The same thing as on QBO: the smallest tool that closes the inbound-PO loop without forcing the customer off their ERP, without a 6-week implementation, and without their PO contents leaving their machine. Distributors don't need another platform — they need their typing to stop. The job hasn't changed because the ERP changed underneath it.
Email [email protected] →