I had a dream ! Managed my Odoo project like any other python project !
Let's see the current state !
A quick word about me: I'm Pierre Verkest, an independent Python & Odoo developer at APYCOD, an OCA contributor, and creator of uvault.
[joke] Let's be honest: for years, setting up Odoo felt like wrestling with custom scripts and long --addons-path configurations.
But today, thanks to tools like uv, pyproject.toml, whool, and hatch-odoo, Odoo fits naturally into standard Python workflows!
Here is what we will cover today:
1. Running Odoo as a standard Python project using uv, PyPI, and namespace packages.
2. Using hatch-odoo to handle flexible addon directories and dynamic dependencies.
3. Managing the real-world pain of unreleased OCA Pull Requests.
4. Deep dive into uvault to safely vault PR dependencies, work locally, and handle release lifecycles.
Let's dive into part one: initializing and managing an Odoo project like any standard Python package.
Before we start, what is uv and why did we choose it?
uv is Astral's ultra-fast Python package and project manager written in Rust. It replaces pip, virtualenv, pyenv, and pip-tools with a single binary.
We choose uv because it's insanely fast, manages Python versions seamlessly, uses global caching with hardlinks so environment syncing takes milliseconds, and locks dependencies deterministically with uv.lock.
Presenter Note: Step 0 (git checkout step-0)
We start with uv init. In just a few milliseconds, uv creates a clean Python repository with pyproject.toml and .python-version.
Notice how clean this structure is—no custom Odoo wrappers, just standard Python config files.
Presenter Note: Step 1 (git checkout step-1)
Next, we add our custom module under src/odoo/addons/ocadays_2026.
In pyproject.toml, we declare our project dependencies and point odoo directly to the OCB 19.0 Git repository.
Presenter Note: Step 1 (continued)
Now comes the first major game changer: [joke] say goodbye to source .venv/bin/activate!
With uv run odoo, uv automatically manages the virtual environment for you.
If a teammate adds a dependency, or if you switch git branches, uv resyncs your .venv in milliseconds before running Odoo. You don't even have to think about it!
Presenter Note: Step 2 (git checkout step-2)
How does Odoo find our custom module? Through PEP 420 namespace packages!
By configuring module-name = "odoo.addons" in pyproject.toml, uv installs our module in editable mode directly into odoo.addons inside .venv.
[joke] That means --addons-path is officially obsolete! Odoo imports our module natively.
Presenter Note: Step 3 (git checkout step-3)
Now, what if we need an OCA addon like mis_builder?
We simply add mis_builder to our module's manifest, and add odoo-addon-mis-builder to pyproject.toml.
When we run uv sync, it automatically fetches the wheel from PyPI along with all its required dependencies.
Presenter Note: (Live Demo hint: show VSCodium sidebar)
Here is a quick pro-tip for your IDE: since all addons live inside .venv/.../site-packages/odoo/addons, add that folder to your IDE workspace!
This unlocks instant global search across all installed OCA addons, jump-to-definition, and allows you to place debug breakpoints anywhere in third-party code.
Presenter Note:
To wrap up part one: uv.lock gives us total reproducibility and supply chain security by locking SHA256 hashes.
In CI, we run uv sync --locked to make sure uv.lock matches pyproject.toml. In production or Docker, we run uv sync --frozen for blazing-fast installs.
Now that our environment is locked and fully reproducible, let's see how we can make Odoo packaging even simpler!
That brings us to section two!
So far, we used Astral's standard uv_build backend, but it forced us into nested folders (src/odoo/addons/) and manual dependency duplication.
Let's see how a dedicated Odoo build backend like hatch-odoo solves this!
Presenter Note: Step 4 (git checkout step-4)
With standard uv_build, we had to use a strict folder layout (src/odoo/addons/).
With hatch-odoo, we set addons_dirs = ["src/odoo/addons"]. You can point to custom_addons, third_party, or multiple directories without deep folder nesting.
Presenter Note:
Under the hood, hatch-odoo creates a .pth file in .venv pointing to a build folder filled with symlinks to all your installable addons.
Python reads the .pth file on startup and exposes them directly inside odoo.addons.
And when you build a production wheel with uv build, it packages everything neatly without symlinks.
Presenter Note: Step 5 (git checkout step-5)
Even better: hatch-odoo supports dynamic dependencies!
Notice how in pyproject.toml, we no longer list any odoo-addon-* packages explicitly!
By setting dynamic = ["dependencies"], hatch-odoo reads the depends field in __manifest__.py and automatically maps them to PyPI packages like odoo-addon-partner-firstname on uv sync.
[joke] You only declare dependencies in one single place—no more keeping __manifest__.py and pyproject.toml in sync manually!
Now, let's address a real-world problem every Odoo developer faces: working with unmerged OCA Pull Requests!
Presenter Note: Step 6 (git checkout step-6)
Very often, a bug fix or new feature is in an open OCA PR that hasn't been merged yet.
With uv, we can add a source override in [tool.uv.sources] pointing directly to refs/pull/827/head.
uv sync pulls the PR code directly from Git and locks the exact commit SHA in uv.lock.
Presenter Note:
Here is how uv handles PR dependencies: pyproject.toml defines the target PR reference, and uv.lock captures the exact git commit SHA.
uv sync then fetches that specific commit to build the environment.
Presenter Note: Step 7 (git checkout step-7)
If you want to edit that OCA PR locally, you clone the repository into .src/ and set path = ".src/..." with editable = true.
Any changes you make locally are reflected immediately in Odoo without reinstalling! No need to hack the addons path!
Presenter Note: Step 8 (git checkout step-8)
Now, here is the trap!
[joke] Raise your hand if a production deployment broke at 3 AM because an external PR author force-pushed or rebased their branch!
If the PR author rebases, the old commit hash is deleted by GitHub's garbage collector.
Your CI build running uv sync --locked fails with the fatal error: "upload-pack: not our ref"!
To summarize: direct Git URLs in pyproject.toml are a ticking time bomb.
First, force-pushes delete commits. Second, PRs can be closed or branches deleted. Third, branch references are not immutable.
So how can we preserve these PR commits under our own control so our builds never break?
Presenter Note:
The solution is to "vault" unmerged PR commits!
Instead of pointing to a volatile PR branch on GitHub, we fetch the PR commit and push an immutable tag (like pjt-<sha>) to a Vault Git repository owned by our organization.
Now, our build depends entirely on our own repository!
Presenter Note:
Even if the PR author force-pushes a brand-new commit, your Vault repository still holds the original commit under your tag.
Your CI and production builds continue running smoothly without breaking!
Presenter Note:
[joke] Vaulting commits manually sounds great on paper, but doing it by hand for 10 or 20 PRs is a recipe for losing your mind.
You have to manually fetch refs, create tags, push to remote vaults, update pyproject.toml, and check PR statuses.
That's why we created a tool to automate all of this: uvault.
Presenter Note: Step 1 (git checkout step-uvault-1)
Instead of manually editing pyproject.toml, you run uvault add with the package name, repo URL, PR number, and subdirectory.
uvault saves your intention under [tool.uvault.sources].
Presenter Note:
Before syncing, we configure project settings in pyproject.toml, such as the tag prefix (e.g. ocadays26) and our Vault repository owner.
Presenter Note: Step 2 (git checkout step-2-uvault)
Now we run uvx uvault sync.
Under the hood, uvault fetches the PR commit, automatically forks the repo into your Vault organization if needed, pushes an immutable tag, and updates [tool.uv.sources]!
Then, a simple uv sync updates uv.lock.
Presenter Note: Step 3 (git checkout step-3-uvault)
How do we track upstream changes? With uvault status!
It queries the GitHub API and shows clear traffic lights:
[joke] Green means the PR was finally merged—time to celebrate and switch back to PyPI!
Red means closed, yellow means open, and warnings alert you if a force-push happened upstream.
Presenter Note: Step 4 (git checkout step-4-uvault)
Need to fix a bug in an OCA addon locally?
Run uvault develop <package> <branch>. It automatically clones the repository into .src/, sets up git remotes, and switches pyproject.toml to editable mode.
[joke] And to prevent committing local editable paths by accident, we provide a uvault-check pre-commit hook that acts as a safety net!
Presenter Note:
When it's time for a production release, run uvault release.
It tags all vaulted dependencies with your release version tag (e.g. ocadays26-19.0.1.0.0).
Then, in CI or Docker, uv sync --frozen --no-dev gives you a 100% deterministic, tamper-proof build.
Presenter Note:
Machine-level configuration sits in ~/.config/uvault/config.toml.
Your GitHub token enables status checks and automatic repo forking.
The [remotes] section lets uvault develop automatically add your personal GitHub fork so you can git push right away.
Presenter Note: Step 5 (git checkout step-5-uvault)
uvault integrates seamlessly with bump-my-version.
When you bump your release version, a pre-commit hook automatically runs uvault release and freezes immutable release tags for deployment.
Presenter Note:
Looking ahead, the roadmap for uvault includes:
- Integrating gitaggregator to merge multiple PRs for a single addon.
- Showing PR diff previews in uvault status.
- Expanding multi-forge support for GitLab, Gitea, and Forgejo.
- And optional auto-sync flags.