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.
On this page
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:
| Path | Contents |
|---|---|
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.txtlists the modules you chose, one name per line.odoo db provisioninstalls these into a new database.pyproject.tomlholds the Python dependencies. odoo-nix writes two managed blocks into it, anduv.lockis 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 addOn 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_groupWhat happens after you choose
Resolve the repos. odoo-nix walks the
dependsof 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.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.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.Regenerate
pyproject.toml. See the next section.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 provisionWarning
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 salesWith 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-repoThe optional second and third arguments are the branch and the submodule folder name under modules/. The rules are:
A string with
://or agit@prefix is used as given, on any host. A scheme-lessowner/repobecomeshttps://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 intomodules.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].dependenciesgetsodooplus oneodoo-addon-<module>entry for each module inmodules.txtthat exists on disk. These are the install roots.[tool.uv.sources]getsodooas a path to the OCB folder, and every module found on disk undermodules/andcustom/as an editable path source. Modules are found by looking for__manifest__.py, either atmodules/<repo>/<module>/ormodules/<module>/, and atcustom/<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-migrateUse --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-submodulesgets those commits, which is what makes a deployment reproducible.Updates ignore the recorded commit.
odoo project updatefetches the tip of the branch named in.gitmodulesand 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 updatefor 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 statusshows 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 --recursiveWhere 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
Run
odoo module addorodoo module add-bundlefrom the project root.Read the output for skipped repos, unknown modules and a failed
uv lock.Run
direnv reload.Run
odoo db provisionfor a new database, or install the module on an existing one.Commit
modules.txt,pyproject.toml,uv.lock,.gitmodulesand the new submodule pointers together.
Related documents
Odoo Development Environment with odoo-nix, for scaffolding the project and the daily
odoocommands.Running Odoo as a NixOS Service, for deploying the project once its modules are chosen.
Sources
This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.