Odoo is the Belgian-origin open-source ERP that grew from a small accounting tool into the largest open-source ERP app marketplace in the world. Python + PostgreSQL stack, modular by design, three editions: Community (free), Enterprise (per-user + per-app subscription), and Odoo.sh (managed PaaS). Latest is Odoo 18; Odoo 19 modules are already appearing in the Apps Store. Native XML-RPC and JSON-RPC; the Apps Store now hosts a growing roster of MCP server modules that expose the ERP as natural-language CRUD for Claude, ChatGPT, and Cursor. The most code-friendly ERP in the open-source field — and the strongest agentic story among open-source ERPs in 2026.
Odoo started as TinyERP in 2005, was renamed OpenERP, then Odoo in 2014. The underlying architecture is a Python web server (long called "OpenObject", now just the Odoo runtime) running on PostgreSQL, with a modular plugin system that lets every business function live as a separately-installable app. The "ERP" most people refer to is actually a curated bundle of standard apps — Sales, CRM, Inventory, Accounting, Manufacturing, HR, Purchase, Project, and so on — built on top of the same platform any third-party module uses.
The same platform supports three editions, three hosting modes, and one extension model. Apps from the Odoo Apps Store install identically whether the org runs Community on a Hetzner VM, Enterprise on a self-managed server, or Odoo.sh as a managed PaaS. That portability is the platform's competitive moat — very few enterprise ERPs offer a credible self-hosted-to-managed continuum without a forklift migration.
| Edition | Cost model | What's included |
|---|---|---|
| Community | Free (LGPLv3); pay for hosting + implementation only | Core ERP: CRM, Sales, Inventory, Purchase, basic Accounting, Project, Manufacturing (community modules). No Studio, no IoT, no advanced accounting, no payroll. |
| Enterprise | Per-user + per-app subscription (USD) | Everything in Community plus full Accounting, Payroll, Field Service, Sign, Subscriptions, IoT, Studio (drag-and-drop customisation), polished dashboards, support contract. |
| Odoo.sh | Enterprise subscription + per-worker hosting | Enterprise feature set, hosted as a managed PaaS by Odoo S.A. Daily backups on two continents, staging environments per branch, Git integration, mailgun-style email built in. |
The official path from Community to Enterprise is documented and supported, but it's not trivial. Some apps replace their community counterparts entirely (Accounting is the classic example). Data migrations have to be done in the right order, and any community-only modules need replacements or rewrites against the Enterprise data model. Plan it as a project, not an upgrade.
Everything you build on Odoo is some combination of these four. Every record in the database is an instance of a Model; every column is a Field; every query returns a Recordset; the ORM is what makes those primitives feel like Python objects instead of SQL.
A Python class that inherits from models.Model. Becomes a Postgres table; gets a CRUD API, a default form view, and access-control hooks for free.
Typed column attribute. Char, Integer, Many2one, One2many, Many2many. Computed fields with @api.depends.
A set-like collection of records. Operations like filtered(), mapped(), sorted() return new recordsets. Single records are just length-1 recordsets.
The layer between Python recordsets and SQL. Handles permissions, multi-company filters, computed-field dependencies, and the access-rights and record-rules system.
A custom Model definition — the canonical shape of any Odoo module's models file:
# models/project_milestone.py from odoo import models, fields, api class ProjectMilestone(models.Model): _name = 'project.milestone' _description = 'Project Milestone' _order = 'date_due asc, name' name = fields.Char('Milestone', required=True) project_id = fields.Many2one('project.project', required=True) date_due = fields.Date('Due') state = fields.Selection([ ('open', 'Open'), ('done', 'Done'), ('cancelled', 'Cancelled'), ], default='open') invoice_count = fields.Integer(compute='_compute_invoice_count') @api.depends('project_id.invoice_ids') def _compute_invoice_count(self): for milestone in self: milestone.invoice_count = len(milestone.project_id.invoice_ids)
Odoo's access-control story has three layers. Groups grant users membership in roles. Access Rights (ir.model.access) say which groups can read / write / create / delete which models. Record Rules (ir.rule) apply row-level filters per group, including the multi-company filter that scopes every query to the user's allowed company set. Multi-company is built into the platform — not a paid add-on.
Odoo has exposed an external API since the OpenERP days. The original surface is XML-RPC; JSON-RPC arrived in 8.0; API keys (so production integrations don't have to send the user password) became GA in 14.0 and are the default for any non-interactive integration in 18.
Authentication — two-step. First authenticate against the common endpoint to get a uid; then use that uid + an API key (or password, in dev) for actual object operations. The API key is created per-user in the Odoo UI and replaces the user's password for external API calls only.
# Python — XML-RPC client, the canonical Odoo integration shape import xmlrpc.client URL = 'https://example.odoo.com' DB = 'example-production' USER = '[email protected]' KEY = 'pk_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # API key, not password # 1. Authenticate common = xmlrpc.client.ServerProxy(f'{URL}/xmlrpc/2/common') uid = common.authenticate(DB, USER, KEY, {}) # 2. Call object methods models = xmlrpc.client.ServerProxy(f'{URL}/xmlrpc/2/object') partners = models.execute_kw( DB, uid, KEY, 'res.partner', 'search_read', [[['is_company', '=', True], ['country_id.code', '=', 'ZA']]], {'fields': ['name', 'vat', 'email', 'phone'], 'limit': 100} )
The same call over JSON-RPC — same semantics, JSON wire format, easier to call from JavaScript / Workers / curl:
// JSON-RPC call to search_read SA partners await fetch('https://example.odoo.com/jsonrpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: { service: 'object', method: 'execute_kw', args: [ 'example-production', uid, 'pk_...', 'res.partner', 'search_read', [[['is_company', '=', true], ['country_id.code', '=', 'ZA']]], { fields: ['name', 'vat', 'email'], limit: 100 } ], }, }), });
Production integrations should never carry a user password. Create a dedicated integration user in Odoo (with the minimum group memberships needed), generate an API key under that user, and use the key. Rotate the key when the integration's secret store rotates. Revoke the key, not the user, if the credential leaks.
The Odoo Apps Store hosts an active and growing roster of MCP server modules — both free and paid, for both Odoo 18 and 19 — that expose the ERP's models as Model Context Protocol tools consumable by Claude Code, ChatGPT, Cursor, and Gemini-based agents. The pattern is: install the module on the Odoo side, run a small Python client that bridges to the MCP transport, point an MCP-aware agent at the bridge.
The common feature set across the credible MCP modules:
Many2one relationships, time-window filters, all expressible in natural language and translated to search_read / read_group calls.The reference shape on the agent side is dead simple — once the MCP server is reachable, the agent's prompt is "list all open SA quotations over R500k due this quarter, and send a follow-up reminder to the salesperson on each one." The agent reasons about which Odoo models to touch, the MCP server enforces access rules, the operations execute through the standard Odoo ORM. No bespoke integration code on either end.
Pre-MCP, every "AI on top of our ERP" project was bespoke: someone wrote a wrapper service that exposed Odoo's API as agent tools, then plumbed it through an LLM. The MCP module pattern collapses that to a configuration step — install on the Odoo side, register on the agent side. Custom logic only enters when business rules need to live outside Odoo's record rules. For SA SIs running on Odoo, this is the single biggest 2025–26 platform shift to be ready for.
Searching the Apps Store for "MCP" returns more than a dozen modules from different publishers, at price points from free to several hundred USD. The quality varies; some are thin wrappers around execute_kw, others ship genuine MCP-server transport implementations with tool descriptions, error mapping, and structured introspection. Before adopting one in production, evaluate the publisher, read the module source (modules are typically Python and inspectable), and check that it exposes only the models you intend an agent to touch — not the whole ORM by default.
The 2nth ERP sub-tree carries both Odoo and ERPNext as leaves. They solve the same overall problem — open-source ERP for businesses that don't want a SAP / Oracle / NetSuite licence — but the trade-offs are clean enough to be decision-grade:
| Concern | Odoo | ERPNext |
|---|---|---|
| Origin | Belgian (Odoo S.A., 2005) | Indian (Frappe Technologies, 2008) |
| Stack | Python + PostgreSQL | Python + MariaDB (Frappe framework) |
| Licence | LGPLv3 (Community) / OEE proprietary (Enterprise) | GPLv3 / MIT (Frappe) |
| Editions | Community (free) / Enterprise (sub) / Odoo.sh (PaaS) | Self-host (free) / Frappe Cloud (managed) |
| Pricing pressure | Enterprise = per-user + per-app, scales fast at headcount | Frappe Cloud = flat infra cost, no per-user |
| App marketplace | ~36,000 apps (community + paid) | Smaller, growing — tighter quality bar |
| Visual customisation | Studio (Enterprise) — drag-and-drop | Bench + developer mode — code-first |
| AI / MCP | Multiple Apps Store MCP modules in 2026 | No native MCP yet; integration via API + custom wrapper |
| Best fit | Mid-market + enterprise; teams that need a large app library | SMB and dev-led teams; doctype-as-code workflow |
If the team's strongest asset is developers who want everything-as-code and a clean self-hosted story, ERPNext is the better cultural fit. If the team needs a polished editor experience, a deep marketplace of paid apps, and a credible managed-hosting offering from the vendor, Odoo is the better fit. The MCP / agent integration story is currently stronger on Odoo — not because the protocol prefers Python (both stacks are Python), but because the Odoo Apps Store has accumulated multiple production-ready MCP modules and the ERPNext ecosystem hasn't shipped equivalent.
Six failure modes that show up the first time an Odoo project moves from sandbox to live.
Enterprise is priced per "internal user" (full ERP access) plus per app. Portal users (limited self-service) are cheaper, but the line between "this is internal" and "this is portal" matters at the contract negotiation, not after. Model the seat count for a 3-year horizon before signing.
Drag-and-drop Studio customisations are stored as data, not code. Major version upgrades (17 → 18 → 19) sometimes require Studio modules to be re-applied because field semantics change. Treat Studio for prototyping; promote to custom modules for anything that must survive multiple upgrade cycles.
store=False are not searchable by default
A compute= field without store=True is recalculated on every access and isn't indexed. Filtering on it in a search() call either fails or scans the whole table. If a computed field needs to be filterable, store it — and write a proper search method as the escape hatch.
execute_kw args/kwargs split is positional
The third argument to execute_kw is a list of positional args; the fourth is a dict of keyword args. Mixing them up is the #1 reason new Odoo integrations fail silently — the call returns an empty list instead of an error.
An integration user's company set determines what records the API returns. An integration that "doesn't see all the data" is usually a multi-company filter, not a permission bug. Either include the integration user in all companies or pass context: {allowed_company_ids: [...]} on the call.
A new Odoo version drops; the Apps Store catches up over weeks. Modules listed as 18.0-compatible may have rough edges; 19.0-compatible may be a single publisher's port. Don't upgrade production to a new major on day one of a third-party-module-critical workload.
SARS localisation. Odoo ships an l10n_za (and l10n_za_* companion) module set covering the SA chart of accounts, the standard VAT codes (15% standard, 0% zero-rated, exempt), and the SARS-aligned tax report. Several SA partners maintain additional modules for PAYE / UIF / SDL on the Payroll side (Enterprise only) and for the standard SA invoice / quote layouts. EFT integration with Standard Bank, FNB, ABSA, and Nedbank is partner-supplied; quality varies.
Self-hosted is the POPIA-clean option. Community or Enterprise running on a SA-resident server (Hetzner Cape Town, Teraco, or an internal datacentre) keeps all personal data inside the country — the easiest Section 19 / Section 72 evidence trail. Odoo.sh is a managed PaaS hosted in regions Odoo S.A. selects; SA-resident hosting is not a default. For POPIA-sensitive deployments, self-hosted is the path of least friction; Odoo.sh is the speed-of-implementation play.
The partner conversation. Odoo's SA partner ecosystem is mature — multiple Gold and Silver partners with dedicated implementation teams. Day-rate pricing is in line with the SAP / Microsoft Dynamics partner field; net cost over a 3-year horizon often comes out lower because the licence cost is meaningfully smaller. The questions to ask any partner: how many Enterprise implementations have they shipped in the relevant industry, how do they handle the Community-to-Enterprise migration, and what's their position on Odoo.sh vs self-hosting for SA clients.
FX exposure. Enterprise pricing is USD per user per month. At R18/USD, a 25-user Enterprise + 5 apps deployment is in the order of R150k+ per year just in licences. The same workload on Community is just hosting + implementation, which has different FX dynamics (server cost in EUR / ZAR; implementation in ZAR). Model both options before locking the architecture.