Skip to content
Skip to the article
In NixOS: 13 articles
NixOS

Add OCA modules to an odoo-nix project

Pick OCA modules or bundles from the odoo-nix catalog, let it add the dependency repos as submodules, and keep modules.txt and the uv lock in sync.

Updated
Applies to
  • odoo-nix
  • Odoo 18.0
  • Odoo 19.0
Tags
  • nixos
  • odoo
  • oca
  • modules
Reading time
11 min

This document explains how odoo module add and odoo module add-bundle work in an odoo-nix project: where the module list comes from, which repos get cloned, what gets written to modules.txt and pyproject.toml, and how to hold a repo at a version you control. Use it when you want to install an OCA module, or when an add did something you did not expect.

How the pieces fit

An odoo-nix project keeps its Odoo code in three places, all relative to the project root:

PathContents
odoo/The OCB source, a git submodule on the branch that matches your Odoo series
modules/One git submodule per OCA (or third-party) repo
custom/Your own modules

Two files describe what you want installed:

  • modules.txt lists the modules you chose, one name per line. odoo db provision installs these into a new database.

  • pyproject.toml holds the Python dependencies. odoo-nix writes two managed blocks into it, and uv.lock is the lock file built from it.

The module catalog is a JSON file shipped inside odoo-nix (data/oca-modules.json). It has one record per module and series, taken from each module's manifest: name, repo, version, whether it is installable, and its depends list. The picker and the dependency resolver both read this file, so they never need the network to answer "which repo has this module?".

Note

odoo module add, odoo module add-bundle and odoo project update only work in the dev shell of a project checkout. The production odoo binary has no git checkout and no modules.txt, so these commands fail there with a message saying so.

Add modules from the catalog

Run these from the project root, inside the dev shell.

Run odoo module add with no arguments to pick interactively:

odoo module add

On a terminal it first asks whether to browse the OCA catalog or add from a git URL. The catalog picker lists every installable module for your project's series, with its repo and a short summary. Modules marked with a star are flagged as applications in their manifest and sort first, but you can select any of them. Type part of a module or repo name to filter, press Tab to mark modules, and press Enter to confirm. Nothing is pre-selected.

If you already know the names, pass them as arguments:

odoo module add account_financial_report repair_order_group

What happens after you choose

  1. Resolve the repos. odoo-nix walks the depends of every module you chose, within your series only. Each module found in the catalog contributes its repo. A dependency that is not in the catalog for your series is assumed to be Odoo core, which OCB already provides, and is skipped. The result is the full set of OCA repos your selection needs.

  2. Add only the missing repos. A repo counts as present if a folder with its name already exists under modules/. For each new repo, odoo-nix checks that the repo has a branch named after your series. If it does, the repo is cloned there and registered as a submodule tracking that branch. If it does not, you get a warning and that repo is skipped.

  3. Record your choices. The modules you named, not their dependencies, are appended to modules.txt. The file is then sorted and de-duplicated, so diffs stay small.

  4. Regenerate pyproject.toml. See the next section.

  5. Re-lock. odoo-nix runs uv lock, then updates the sdist build-dependency block from the lock file and locks again.

When it finishes, reload the environment so Nix re-derives addons_path and rebuilds the Python environment, then install:

direnv reload
odoo db provision

Warning

Module names you pass as arguments are not checked against the catalog. A typo, or a module that does not exist for your series, still lands in modules.txt. No repo is added for it, and the Python step prints a note that the module was not found on disk and skipped. Check the output of the add before you move on.

Note

odoo db provision installs modules.txt into a database that does not exist yet. On a database that already exists, it migrates instead and does not install anything new. For a database that is already in use, install the new module from the Apps menu after updating the apps list. You can also use odoo-bin's own -i flag, because odoo passes unknown subcommands and flags through to odoo-bin.

Add a curated bundle

A bundle is a named list of modules defined in data/oca-bundles.json inside odoo-nix. The bundles in this version of the file are base, oca-accounting, project, purchase and sales.

odoo module add-bundle
odoo module add-bundle base sales

With no arguments on a terminal you get a picker showing each bundle's name, label and module count. In a script you must pass the names. odoo-nix expands the bundles into one list of modules, drops any that have no catalog record for your series (it prints which ones), and then runs the same flow as odoo module add.

Bundles are plain JSON, so changing one is an edit to the file. If you want a set of your own, that is a change to odoo-nix itself, not to your project.

Add a repo that is not in the catalog

Give odoo module add a git URL or owner/repo GitHub shorthand and it adds that repo as a submodule instead:

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

The optional second and third arguments are the branch and the submodule folder name under modules/. The rules are:

  • A string with :// or a git@ prefix is used as given, on any host. A scheme-less owner/repo becomes https://github.com/owner/repo.git. Anything else is rejected.

  • The folder name defaults to the last part of the URL, and the command refuses a folder that already exists or is already registered.

  • The branch defaults to your Odoo series. If the repo has no such branch, odoo-nix finds the remote's default branch. On a terminal it asks you to confirm that branch. In a script it uses it and says so. A branch you name explicitly must exist on the remote.

  • odoo-nix scans the clone for __manifest__.py, either at the repo root or one folder down. If it finds one module it records it. If it finds several, it asks which to record, and in a script it records all of them. If it finds none, the submodule is still added and nothing goes into modules.txt.

Warning

The git URL path does not resolve dependencies. If a third-party module depends on an OCA module whose repo you do not have yet, add that module with odoo module add <MODULE_NAME> as well.

What goes into pyproject.toml

OCA modules are packaged with whool. Each module's metadata names its dependencies, and uv can read it. So odoo-nix does not translate manifests into requirements itself. It only tells uv where every module lives, and uv resolves the rest.

lib/oca_sources.py rewrites two blocks, each between marker comments that start with # >>> odoo-nix::

  • [project].dependencies gets odoo plus one odoo-addon-<module> entry for each module in modules.txt that exists on disk. These are the install roots.

  • [tool.uv.sources] gets odoo as a path to the OCB folder, and every module found on disk under modules/ and custom/ as an editable path source. Modules are found by looking for __manifest__.py, either at modules/<repo>/<module>/ or modules/<module>/, and at custom/<module>/.

Everything inside the markers is overwritten on every run, so do not edit it by hand. Anything outside the markers, such as the [dependency-groups] block, is left alone. A third marker pair holds build dependencies for packages that only ship as sdists, and it is maintained from uv.lock for you.

If uv cannot resolve the environment, the add still finishes, prints a warning that uv lock failed, and leaves modules.txt and the submodules in place. Fix the conflict in pyproject.toml and lock again. For a real version conflict between packages, the odoo-nix README suggests uv's override-dependencies setting under [tool.uv], which sits outside the managed blocks.

Update modules and re-lock

odoo project update moves every submodule to the latest commit of its branch, regenerates the two blocks in pyproject.toml, and runs uv lock. By default it then migrates every database that matches dbfilter.

odoo project update
odoo project update --no-migrate

Use --no-migrate when you want to look at what changed before touching a database.

This is also the way to pick up a hand edit. If you edit modules.txt yourself, for example to remove a module, odoo project update is the command in the CLI that regenerates pyproject.toml from it. The CLI has no command that removes a module or a submodule, and this guide has not verified a removal workflow, so treat that as a manual git operation and review the diff.

Pins, branches and submodules

Each submodule has two facts that matter: the commit your project records, and the branch named in .gitmodules.

  • Series branch. New OCA repos are added on the branch that matches your series, such as 18.0.

  • Recorded commit. Git records an exact commit for each submodule in your project. Anyone who clones the project with --recurse-submodules gets those commits, which is what makes a deployment reproducible.

  • Updates ignore the recorded commit. odoo project update fetches the tip of the branch named in .gitmodules and checks it out. If you had checked out a specific older commit in a submodule, the update replaces it.

That last point decides how you hold a repo back. The CLI has no per-repo pin. These are the options the code supports:

  • Do not run odoo project update for that project until you are ready, and commit the submodule move when you are.

  • Point the submodule at a branch you control. Fork the repo, add it with odoo module add owner/repo <BRANCH> <FOLDER>, and keep that branch where you want it. The update then follows your branch.

  • Review the update before you commit it. After odoo project update --no-migrate, git status shows which submodules moved, and you can revert one you do not want.

Tip

After an update, run git diff --submodule=log from the project root to see the commits that came in for each repo before you commit the new pointers.

How submodules are cloned

Submodules are cloned as partial clones: every commit and folder of the series branch, with file contents downloaded as you check them out. The commits of every other branch are fetched too, so git switch to another branch works. OCB is the exception, because its folder history is very large, so it keeps commits only. The first time you enter the dev shell in a new clone, odoo-nix checks out any submodule that clone has never had. It does not bring back one you removed on purpose, and tells you how to restore it instead.

If a submodule folder is registered but empty, the shell tells you, and this command checks out the commits your project records:

git submodule update --init --recursive

Where OCB comes from

In some projects OCB is not a submodule but a flake input, set through the coreSource option. In that case odoo project update does not move it. It prints a reminder to run nix flake update instead, and the lock step temporarily points odoo/ at a writable copy so that uv can read it.

Checklist

  1. Run odoo module add or odoo module add-bundle from the project root.

  2. Read the output for skipped repos, unknown modules and a failed uv lock.

  3. Run direnv reload.

  4. Run odoo db provision for a new database, or install the module on an existing one.

  5. Commit modules.txt, pyproject.toml, uv.lock, .gitmodules and the new submodule pointers together.

Sources

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