Skip to content
Skip to the article
In Odoo: 7 articles
Odoo

Set up and work in an Odoo 18 development environment with odoo-nix

Repository layout, daily commands, adding OCA modules, running a dev server with a mail catcher, and upgrading modules in an odoo-nix managed Odoo 18 project.

Updated
Applies to
  • Odoo 18.0 (OCB)
  • OCA 18.0 modules
  • odoo-nix
  • devenv
  • PostgreSQL 16
Tags
  • odoo
  • devenv
  • nix
  • development
Reading time
11 min

This guide is for a developer joining an Odoo 18 Community project that uses OCA modules and is managed with odoo-nix. It covers how the repository is laid out, which commands you run every day, how to add OCA modules, how to run the dev server, and how to apply code changes to a database.

Production hosting is a separate topic and is not covered here. For how the module set is chosen, read Odoo Community and OCA Overview first.

What you get

odoo-nix turns the project into one reproducible shell. You need Nix with flakes enabled and direnv. Everything else comes from the flake:

  • PostgreSQL 16, started for you by devenv.

  • The Odoo server, running from source with --dev=all.

  • Mailpit, a local mail catcher.

  • A pinned Python environment (Python 3.11 for Odoo 18) built from pyproject.toml and uv.lock.

  • One odoo command for database, module and project tasks. Anything it does not know is passed straight to the real odoo-bin.

Repository layout

PathWhat it is
odoo/The OCB 18.0 source, a git submodule.
modules/OCA repositories, one git submodule per repository.
custom/The project's own modules.
modules.txtThe install set: one module name per line.
pyproject.toml, uv.lockThe Python environment, resolved by uv.
flake.nixA thin wrapper that sets project name, series, database name and so on.
odoo.confA symlink into the Nix store. Generated, never edited.
.venv, odools.tomlSymlinks for editors. Generated.
.devenv/state/The development PostgreSQL cluster and the Odoo filestore. Not tracked.

Three of these deserve a closer look.

modules.txt

odoo db provision installs base plus every module listed in modules.txt. The file is also what the Python environment is built from: each listed module becomes an install root in pyproject.toml, and uv resolves the dependency graph from there. A module listed here must exist on disk, either under modules/ or under custom/.

odoo module add keeps the file sorted and free of duplicates. Many projects keep it to OCA modules only and install their own modules from custom/ explicitly, but nothing in odoo-nix forces that convention. Follow whatever your project already does.

pyproject.toml

Three blocks in pyproject.toml are managed by odoo-nix and sit between marker comments that start with # >>> odoo-nix:: the install set, the module sources, and the build dependencies for packages that only ship as source. Do not edit anything between the markers. odoo-nix regenerates them whenever you run odoo module add, odoo module add-bundle or odoo project update.

The [dependency-groups].dev block is yours. It already carries watchdog (for code reload), freezegun and websocket-client (both needed to run Odoo tests). Leave those in. Without freezegun no Odoo test can run at all.

odoo.conf

The file is rendered from your flake settings and the folders on disk. addons_path is derived from what exists, in this order: the two core OCB roots, each OCA repository under modules/ that contains a module, then custom/. You never maintain that list by hand.

To change a setting such as the database name or the HTTP port, change the odooConf options in flake.nix, run direnv reload, and restart devenv up.

Start working

On a fresh clone, take the submodules along and let direnv build the shell:

git clone --recurse-submodules <REPO_URL> <PROJECT_DIR>
cd <PROJECT_DIR>
direnv allow

If you do not use direnv, nix develop --no-pure-eval gives you the same shell. The first entry also checks out any submodule you have never had, so a clone made without --recurse-submodules still works.

Run the dev server

Start PostgreSQL, Odoo and Mailpit together:

devenv up

In a second terminal, in the same project directory, create the database and install the install set:

odoo db provision

With no argument, the command uses the database name configured in odoo.conf. It is safe to run again: on a database that already exists it migrates instead of reinstalling.

When it finishes, open Odoo at http://localhost:8069 and the mail catcher at http://localhost:8025.

Mail never leaves your machine

In the dev shell every outgoing email is redirected to Mailpit. This is done by a small server-wide addon that ships with odoo-nix, so it covers every database on the server and cannot be bypassed by an outgoing mail server record. It is never installed into a database and never copied into your repository. At startup the Odoo log warns that all outgoing email is being redirected.

Use Mailpit to check invitations, quotation emails and notifications. The catch-all exists only in the dev shell. It is not part of the server or container builds.

Live reload

Edits are picked up without restarting devenv up:

  • A change to a .py file anywhere on the addons_path restarts the server in place. A syntax error is logged and the restart is skipped until you fix it.

  • XML views, QWeb templates and JS or SCSS assets are re-read from disk. Reload the browser.

  • Odoo framework code under odoo/odoo/ is not watched. Restart devenv up after changing it.

Look for AutoReload watcher running with watchdog in the log to confirm the watcher is on. Reload only works with workers = 0, which is the dev default.

Note

Reload picks up code, not schema. A new field, a changed data file or a migration script only takes effect once the module is upgraded. See Apply code changes to a database.

Daily commands

Run these from the project directory inside the dev shell.

CommandWhat it does
devenv upStarts PostgreSQL, Odoo and Mailpit.
odoo db provision [DB]Creates the database and installs modules.txt, or migrates it if it exists.
odoo db upgrade <MODULE[,MODULE]> [DB]Upgrades the named modules, with a progress bar and a summary table.
odoo db migrate [DB]Upgrades only the modules whose code or version changed.
odoo shell [DB]Opens the Odoo Python shell.
odoo test <MODULE[,MODULE]> [DB]Runs module tests.
odoo module add [MODULE ...]Adds OCA modules and their repositories.
odoo project updatePulls submodules, refreshes dependencies, re-locks, then migrates.
odoo db backup [DB]Backs up a database (schema and filestore by default).
odoo db listLists databases.

Commands that take a database fall back to the one in odoo.conf. The db upgrade, db migrate and db backup commands also accept --all for every database matching dbfilter.

Add OCA modules

odoo module add resolves which OCA repositories a module needs, adds only the missing ones as submodules under modules/, records the modules in modules.txt, regenerates the managed blocks in pyproject.toml and re-locks.

odoo module add                           # interactive picker
odoo module add <MODULE_NAME>             # by name
odoo module add-bundle                    # pick a curated bundle
odoo module add-bundle base sales         # bundles by name

The picker lists every installable module in the bundled catalog for your series, not just applications. Bundles live in data/oca-bundles.json in the odoo-nix repository and include base, oca-accounting, project, purchase and sales.

You can also add a repository that is not in the OCA catalog, from any git host:

odoo module add https://example.com/owner/repo.git
odoo module add owner/repo 17.0 my-repo

The second form is GitHub shorthand with an explicit branch and a submodule folder name. When you leave the branch out, odoo-nix uses your Odoo series and falls back to the repository's default branch if that series does not exist.

After any add, reload the shell so Nix re-derives addons_path and rebuilds the Python environment:

direnv reload

Then restart devenv up. Adding a module does not install it into a database you already have. On an existing database, odoo db provision only updates what is installed, so install the new modules from the Apps menu (use Update Apps List first so Odoo notices them). A brand-new database picks up everything in modules.txt when you run odoo db provision.

Check git status and commit the whole result together: .gitmodules, the new submodule folders, modules.txt, pyproject.toml and uv.lock.

Tip

Treat a submodule update as a code change. OCA modules vary in maturity, so after odoo project update smoke-test the features you depend on before you commit the new submodule pointers.

Apply code changes to a database

Odoo creates columns, reloads data files and runs migration scripts only for modules being updated. Changed code without an update leaves the schema silently behind the code. You have two tools.

Upgrade specific modules

When you know which module you changed:

odoo db upgrade my_module
odoo db upgrade my_module,my_other_module <DB>

Upgrade whatever changed

After pulling a teammate's work, or after git submodule pointers move, let odoo-nix work out the set:

odoo db migrate

It compares each module's content checksum with the one stored the last time a migration succeeded, and each module's recorded version with its manifest. It prints the plan before it does anything. Before updating it takes a snapshot of the database (a zip with the filestore, by default). Useful options:

  • --full updates every installed module instead of only the changed ones.

  • --also <MODULE,MODULE> forces specific modules into the update.

  • --snapshot zip|dump|none chooses the snapshot format, or skips it.

  • --rollback restores the snapshot if the update fails.

The first migration on a database with no stored checksums updates every module once to set a baseline. odoo db provision records that baseline for a fresh install, so you only meet it on a database restored from elsewhere. The checksums use the same algorithm as the OCA module_auto_update module.

odoo project update runs the same migration across every database matching dbfilter after it updates the code. Pass --no-migrate to skip that step.

Run tests

This is the short version. Testing Custom Odoo Modules covers tags, phases and writing tests that survive upgrades.

odoo test my_module <TEST_DB>

Use a separate database from the one you click around in. The command upgrades the module with -u, so the module must already be installed in that database. It refuses to run if the headless browser or the websocket-client package is missing, because Odoo would otherwise skip browser tours and report that as a pass. You can override that with ODOO_TEST_ALLOW_SKIP=1.

If the dev server is running, the test run clashes with its ports. Use the odoo-bin passthrough with different ports instead:

odoo -d <TEST_DB> -u my_module --test-enable --test-tags /my_module --stop-after-init --http-port 8098 --gevent-port 8097

Everything else

Any command the odoo wrapper does not know goes to odoo-bin unchanged, so scaffold, populate, cloc and the rest still work. Database housekeeping lives under odoo db:

odoo db duplicate <SOURCE_DB> <NEW_DB>
odoo db restore <DB> <BACKUP_FILE> --force
odoo db drop <DB> --yes

Backups land in .devenv/state/odoo/backups/<DB>/ unless you pass --path. Take one before experiments.

Editor setup

The shell links ./.venv to the Nix-built Python environment and generates odools.toml for the Odoo language server. For VS Code it seeds .vscode/settings.json and a recommendation for the Odoo.odoo extension, once, and never overwrites your own files. Other editors can point their language server at ./.venv and the .devenv/state/pythonpath folder.

Start a new project

If you are starting from nothing rather than joining a project, the scaffolder creates the layout above, adds OCB and your chosen OCA repositories as submodules, and locks the Python environment:

nix run github:Avunu/odoo-nix -- --series 18.0 --name <PROJECT_NAME> --db <DB_NAME> <TARGET_DIR>

Add --bundles <NAME> or --modules <MODULE,MODULE> to preselect OCA content. Without them you get plain Odoo core, and you can add modules later with odoo module add.

Troubleshooting

  • A module you just added is not found. Run direnv reload so addons_path is regenerated, restart devenv up, then use Update Apps List in Odoo.

  • Python changes do not reload. Check the log for the AutoReload watcher line, and confirm workers is 0.

  • A new field is missing in the UI or errors in the database. Upgrade the module with odoo db upgrade, or run odoo db migrate.

  • uv lock fails after adding a module. Fix the cause reported in the output, then run odoo project update to regenerate the managed blocks and lock again.

Sources

This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.