44from typing import Annotated
55
66import typer
7+ from odoo_addons_path import (
8+ detect_codebase_layout ,
9+ get_addons_path ,
10+ get_odoo_version_from_release ,
11+ )
712
813from odoo_venv .exceptions import PresetNotFoundError
914from odoo_venv .launcher import create_launcher
@@ -48,6 +53,20 @@ def preset_callback(ctx: typer.Context, param: typer.CallbackParam, value: str):
4853 return value
4954
5055
56+ def project_dir_callback (ctx : typer .Context , param : typer .CallbackParam , value : str | None ):
57+ if not value :
58+ return None
59+
60+ # Auto-apply "project" preset if no preset was explicitly set.
61+ # --preset is also is_eager and declared before --project-dir, so if the user
62+ # passed --preset explicitly, ctx.default_map is already populated here.
63+ if not ctx .default_map :
64+ preset_callback (ctx , param , "project" )
65+
66+ ctx .ensure_object (dict )["project_dir" ] = value
67+ return value
68+
69+
5170def version_callback (value : bool ):
5271 if value :
5372 typer .echo (f"odoo-venv { version ('odoo-venv' )} " )
@@ -70,10 +89,69 @@ def main_callback(
7089 pass
7190
7291
92+ def _detect_project_layout (project_dir_value : str ) -> tuple [Path | None , str | None , str | None ]:
93+ """Detect odoo_dir, odoo_version, and addons_path from a project directory.
94+
95+ Returns:
96+ (odoo_dir_path, odoo_version, addons_path) — any value may be None if not detected.
97+ """
98+ project_dir_path = Path (project_dir_value ).expanduser ().resolve ()
99+ detected_paths = detect_codebase_layout (project_dir_path )
100+
101+ addons_path = get_addons_path (project_dir_path , detected_paths = detected_paths )
102+
103+ # Resolve odoo_dir from detected layout
104+ odoo_dir_path = None
105+ if detected_paths .get ("odoo_dir" ):
106+ odoo_dir_path = detected_paths ["odoo_dir" ][0 ].parent
107+
108+ # Infer version from release.py inside the detected odoo dir
109+ odoo_version = None
110+ if odoo_dir_path :
111+ odoo_version = get_odoo_version_from_release (odoo_dir_path )
112+
113+ return odoo_dir_path , odoo_version , addons_path
114+
115+
116+ def _resolve_odoo_dir_and_version (
117+ odoo_dir : str | None ,
118+ odoo_version : str | None ,
119+ detected_odoo_dir : Path | None ,
120+ detected_version : str | None ,
121+ ) -> tuple [Path , str ]:
122+ """Determine odoo_dir and odoo_version from explicit args or detected values.
123+
124+ Priority: explicit CLI flags > auto-detected from --project-dir > default path.
125+ Exits with an error if neither can be resolved.
126+ """
127+ # Resolve odoo_dir: explicit flag > detected > default path from version
128+ if odoo_dir :
129+ odoo_dir_path = Path (odoo_dir ).expanduser ().resolve ()
130+ elif detected_odoo_dir :
131+ odoo_dir_path = detected_odoo_dir
132+ elif odoo_version :
133+ odoo_dir_path = Path (f"~/code/odoo/odoo/{ odoo_version } " ).expanduser ()
134+ else :
135+ typer .secho ("error: ODOO_VERSION is required when --project-dir is not used." , fg = typer .colors .RED )
136+ raise typer .Exit (1 )
137+
138+ # Resolve odoo_version: explicit arg > detected from release.py
139+ resolved_version = odoo_version or detected_version
140+ if not resolved_version :
141+ typer .secho (
142+ "error: Could not detect Odoo version from source. Provide ODOO_VERSION explicitly." , fg = typer .colors .RED
143+ )
144+ raise typer .Exit (1 )
145+
146+ return odoo_dir_path , resolved_version
147+
148+
73149@app .command ()
74150def create (
75151 ctx : typer .Context ,
76- odoo_version : Annotated [str , typer .Argument (help = "Odoo version, e.g: 18.0" )],
152+ odoo_version : Annotated [
153+ str | None , typer .Argument (help = "Odoo version, e.g: 18.0. Inferred from --project-dir if omitted." )
154+ ] = None ,
77155 python_version : Annotated [
78156 str | None ,
79157 typer .Option ("--python-version" , "-p" , help = "Specify Python version." ),
@@ -154,12 +232,27 @@ def create(
154232 help = "Generate a launcher script in ~/.local/bin/." ,
155233 ),
156234 ] = False ,
235+ project_dir : Annotated [
236+ str | None ,
237+ typer .Option (
238+ "--project-dir" ,
239+ callback = project_dir_callback ,
240+ is_eager = True ,
241+ help = "Path to project directory. Auto-detects --addons-path, --odoo-dir "
242+ "via odoo-addons-path and applies --preset=project." ,
243+ ),
244+ ] = None ,
157245):
158246 """Create virtual environment to run Odoo"""
159- if not odoo_dir :
160- odoo_dir_path = Path (f"~/code/odoo/odoo/{ odoo_version } " ).expanduser ()
161- else :
162- odoo_dir_path = Path (odoo_dir ).expanduser ().resolve ()
247+ # Auto-detect layout from --project-dir if provided
248+ project_dir_value = ctx .obj .get ("project_dir" ) if ctx .obj else None
249+ detected_odoo_dir , detected_version , detected_addons_path = (
250+ _detect_project_layout (project_dir_value ) if project_dir_value else (None , None , None )
251+ )
252+
253+ odoo_dir_path , odoo_version = _resolve_odoo_dir_and_version (
254+ odoo_dir , odoo_version , detected_odoo_dir , detected_version
255+ )
163256
164257 if not python_version :
165258 python_version = ODOO_PYTHON_VERSIONS .get (odoo_version )
@@ -173,6 +266,9 @@ def create(
173266 else :
174267 extra_requirements_list = list (extra_requirement )
175268
269+ if not addons_path and detected_addons_path :
270+ addons_path = detected_addons_path
271+
176272 addons_path_list = (
177273 [str (Path (p .strip ()).expanduser ().resolve ()) for p in addons_path .split ("," )] if addons_path else None
178274 )
0 commit comments