What’s inside an Odoo addon
An Odoo module is a Python package with a manifest. That’s it. The rest is convention, and following the convention is what makes a module easy for the next developer to pick up. Here’s the layout erpfly generates for the calibration prompt at the top of this page:
gauge_calibration/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ ├── calibration_gauge.py
│ └── mrp_workorder.py # _inherit, blocks overdue gauges
├── security/
│ ├── security.xml # groups and record rules
│ └── ir.model.access.csv
├── data/
│ └── ir_cron.xml # daily due-date check
├── views/
│ ├── calibration_gauge_views.xml
│ └── menus.xml
└── tests/
└── test_calibration.py
A few details decide whether an addon installs cleanly or throws errors on a real database. The data list in __manifest__.py is loaded in order, so security groups come before the CSV that references them, and actions come before the menus that open them. Every new model gets a line in ir.model.access.csv. Without one, recent Odoo versions warn at install and ordinary users can’t open the model at all, which is a confusing first day for everyone. And depends lists what the code really uses, mrp and mail here, not every app that happened to be installed.
If you’d rather build one by hand first, our walkthrough on creating a custom Odoo module step by step covers the same structure line by line.
One codebase per Odoo version
Odoo changes enough between major versions that “compatible with 17, 18 and 19” usually means three branches. The differences that bite most often:
- Odoo 17 removed
attrsandstatesfrom views. Visibility is now a Python-like expression, as ininvisible="state != 'draft'". - Odoo 17 also replaced
name_getwith_compute_display_name, which you’ll see at the bottom of the model on this page. - Odoo 18 renamed
<tree>views to<list>, in view definitions and in action view modes alike. Anything still using the old name has to be updated. - The web client is built on OWL throughout, so any custom widget from the old widget system has to be rewritten, not ported.
erpfly asks for your version before writing a line, and the manifest version string follows the usual 18.0.1.0.0 pattern so the major version is visible at a glance.
Community, Enterprise and what you can depend on
This decision shapes the module more than people expect. Enterprise is Community plus a set of extra addons. If your module inherits from an Enterprise model, say quality.check or anything in Helpdesk, it only installs where those addons exist.
On Community, the same feature is often built on top of standard stock and manufacturing models, or on modules from the Odoo Community Association. It starts from a different place, and it isn’t a worse module for it. What we won’t do is copy Enterprise code into a Community addon. It’s licensed differently, and that shortcut ends in a very awkward email.
Deploying on Odoo.sh or your own server
Odoo.sh deploys from GitHub branches. Push the addon to a development branch and Odoo.sh builds it on a fresh database and runs the tests. Merge into a staging branch and it runs against a neutralized copy of production. Merge into production and the module updates on the live database. Odoo.sh needs an Enterprise subscription, which is worth knowing before you plan around it.
Self-hosted is the familiar route: add the addon’s folder to addons_path, restart, then install from Apps or with -i gauge_calibration. Updates run with -u, and the generated tests run with --test-enable and a test tag for the module, so a broken rule shows up before users find it. Test on a copy of production first. Most failed installs aren’t bugs in the code, they’re existing records that don’t satisfy a new constraint.
Example: calibration in a machine shop
Picture a shop making precision parts, with a couple of hundred gauges, micrometers and bore gauges tracked in a spreadsheet that one quality engineer updates when she remembers. An auditor asks which gauge measured a batch from last March and the spreadsheet can’t answer.
Before writing anything, erpfly asks two questions a good consultant would. Can a gauge be used while its calibration is at an outside lab? And does overdue mean blocked immediately, or after a grace period? The answers change the model, so they’re worth the thirty seconds.
The module from the prompt fixes the part that matters. Gauges get a record with a stored next due date. A daily ir.cron marks them due and schedules an activity for the quality team. Work orders _inherit a check that refuses to start if a linked gauge is overdue. The certificate PDF sits on the gauge record, and the chatter keeps the history the auditor was asking for. If your quality process runs wider than calibration, look at the quality management module and how it ties into manufacturing.
When a new module is the wrong answer
- You only need a few fields on an existing screen. That’s inheritance on an existing model, and our Odoo customization page covers it. A whole addon for three fields is still fine, but don’t call it a module project.
- A maintained OCA module already does it. Install it, read the code, contribute a fix if needed. Cheaper than owning a copy.
- You’re on Odoo Online and don’t want to move. You can’t install custom Python there. Decide on Odoo.sh or self-hosting first, or read our view on Studio versus a custom module.
And if the choice of platform is still open, our ERPNext vs Odoo comparison is blunt about where each one fits.