Skip to content
Odoo 17 · 18 · 19

Odoo custom module development, from a plain-English brief to an addon you can install

Sooner or later every Odoo company needs something the Apps store doesn't sell, or sells for a version two releases behind yours. erpfly writes that addon from your description, with the manifest, models, views and access rules a reviewer expects, and hands it over as a Git branch.

Odoo 19
  • Code in your Git repo
  • Tested on a sandbox first
  • No core files edited

Deliverables

What you walk away with

Not a demo, not a mockup. Files you can open, change and deploy without us.

A standard addon layout

__manifest__.py with a real version string and a complete depends list, then models, views, security and data folders. Nothing an experienced Odoo developer would need to rearrange.

Models that follow the ORM

_name for new models, _inherit for extending existing ones, stored computed fields with @api.depends, constraints, and mail.thread where a chatter earns its place.

Views written for your version

Form, list, kanban and search views using inline invisible and readonly expressions, and list instead of tree on Odoo 18 and 19.

Access rules that aren't wide open

ir.model.access.csv lines per group, plus record rules for multi-company databases, instead of giving every internal user full rights on every model.

Tests you can run in CI

TransactionCase tests for the business rules, tagged so they run with --test-tags on Odoo.sh builds or your own pipeline.

The process

How the work actually goes

  1. 1

    Tell us version and edition

    Odoo 17, 18 or 19, Community or Enterprise, Odoo.sh or self-hosted. These decide which modules you can depend on and what the XML is allowed to look like.

  2. 2

    Describe the process

    Write it the way you'd explain it to a new production planner. erpfly asks follow-up questions where the rules are ambiguous, like who can mark a gauge as passed.

  3. 3

    Try it on a throwaway database

    The addon is installed on a fresh database with the same apps you run. Click through, create test records and ask for changes in the chat.

  4. 4

    Push to a branch

    The code lands in your GitHub repository. On Odoo.sh a development branch builds automatically, and you promote it to staging and production when you're happy.

gauge_calibration/models/calibration_gauge.py
from datetime import timedelta

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class CalibrationGauge(models.Model):
    _name = "calibration.gauge"
    _description = "Measuring Gauge"
    _inherit = ["mail.thread", "mail.activity.mixin"]
    _order = "next_due_date"

    name = fields.Char(required=True, tracking=True)
    serial_no = fields.Char(string="Serial Number", required=True, copy=False)
    interval_days = fields.Integer(string="Interval (days)", default=180)
    last_calibrated = fields.Date(tracking=True)
    next_due_date = fields.Date(compute="_compute_next_due_date", store=True)
    certificate = fields.Binary(attachment=True)
    company_id = fields.Many2one("res.company", default=lambda self: self.env.company)

    @api.depends("last_calibrated", "interval_days")
    def _compute_next_due_date(self):
        for gauge in self:
            gauge.next_due_date = gauge.last_calibrated and (
                gauge.last_calibrated + timedelta(days=gauge.interval_days))

    @api.constrains("interval_days")
    def _check_interval(self):
        if any(gauge.interval_days <= 0 for gauge in self):
            raise ValidationError(_("Calibration interval must be at least one day."))

    @api.depends("name", "serial_no")
    def _compute_display_name(self):
        for gauge in self:
            gauge.display_name = f"{gauge.name} [{gauge.serial_no}]"
An excerpt from a generated module. Trimmed for the page.

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 attrs and states from views. Visibility is now a Python-like expression, as in invisible="state != 'draft'".
  • Odoo 17 also replaced name_get with _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.

Modules people build with this

Guides and terms for this work

Questions people ask us

Something missing? Email hello@erpfly.com and a person will answer.

Which Odoo versions do you generate code for?

Odoo 17, 18 and 19. The addon is written for the version you pick, because the view syntax and a few ORM methods changed between them. If you're on 16 or older, we'd rather plan the upgrade with you than write new code for a version that's about to lose support.

Will the module work on Odoo Online?

No, and nobody's module will. Odoo Online doesn't let you install your own Python addons. You need Odoo.sh or your own server. On Odoo Online the only option is Studio, which covers fields and simple views but not real business logic.

Can the addon depend on Enterprise apps?

Only if you have an Enterprise subscription. An addon that lists quality_control or helpdesk in its depends won't install on Community. erpfly checks your edition first and, on Community, builds on standard Community modules or OCA ones instead.

Who maintains the module when Odoo releases a new version?

Odoo's upgrade service migrates the standard database for Enterprise customers, but your own addons are your responsibility. erpfly can regenerate or port the addon to the next version, and the tests show you what changed behaviour.

Do we own the code?

Yes. It's a normal Odoo addon in your repository, licensed however you choose (we default to LGPL-3 to match Community). Stop using erpfly and the module keeps working.

Other ways we can help

Your next module is one paragraph away

Write it the way you’d explain it to a new hire. We’ll turn it into an app you can read, test and install.

Odoo 19

Create your account

Free to start. No card needed.

By signing up you agree to our terms and privacy policy.