odoo-dev 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo_dev/__init__.py +3 -0
- odoo_dev/cli.py +36 -0
- odoo_dev/commands/__init__.py +1 -0
- odoo_dev/commands/db.py +332 -0
- odoo_dev/commands/docker.py +196 -0
- odoo_dev/commands/run.py +432 -0
- odoo_dev/commands/setup.py +364 -0
- odoo_dev/config.py +101 -0
- odoo_dev/utils/__init__.py +1 -0
- odoo_dev/utils/console.py +25 -0
- odoo_dev-0.1.0.dist-info/METADATA +133 -0
- odoo_dev-0.1.0.dist-info/RECORD +14 -0
- odoo_dev-0.1.0.dist-info/WHEEL +4 -0
- odoo_dev-0.1.0.dist-info/entry_points.txt +2 -0
odoo_dev/commands/run.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Local Odoo runtime commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from odoo_dev.config import load_config
|
|
12
|
+
from odoo_dev.utils.console import error, info, success, warning
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(
|
|
16
|
+
db_name: Annotated[
|
|
17
|
+
str | None, typer.Option("-d", "--database", help="Database name")
|
|
18
|
+
] = None,
|
|
19
|
+
install: Annotated[
|
|
20
|
+
str | None, typer.Option("-i", "--init", help="Modules to install (comma-separated)")
|
|
21
|
+
] = None,
|
|
22
|
+
update: Annotated[
|
|
23
|
+
str | None, typer.Option("-u", "--update", help="Modules to update (comma-separated)")
|
|
24
|
+
] = None,
|
|
25
|
+
dev: Annotated[
|
|
26
|
+
str | None,
|
|
27
|
+
typer.Option("--dev", help="Dev mode options (e.g., 'reload,qweb,xml')"),
|
|
28
|
+
] = None,
|
|
29
|
+
debug: Annotated[
|
|
30
|
+
bool, typer.Option("--debug", help="Enable debugpy for VSCode debugging")
|
|
31
|
+
] = False,
|
|
32
|
+
port: Annotated[
|
|
33
|
+
int, typer.Option("-p", "--port", help="HTTP port")
|
|
34
|
+
] = 8069,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Run Odoo locally."""
|
|
37
|
+
cfg = load_config()
|
|
38
|
+
|
|
39
|
+
# Verify setup
|
|
40
|
+
if not cfg.venv_path.exists():
|
|
41
|
+
error(f"Virtual environment not found at {cfg.venv_path}")
|
|
42
|
+
error("Run 'odoo-dev setup' first.")
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
|
|
45
|
+
if not cfg.config_file.exists():
|
|
46
|
+
error(f"Config file not found at {cfg.config_file}")
|
|
47
|
+
error("Run 'odoo-dev setup' first.")
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
|
|
50
|
+
if not cfg.odoo_bin.exists():
|
|
51
|
+
error(f"Odoo not found at {cfg.odoo_bin}")
|
|
52
|
+
error("Run 'odoo-dev setup' first to clone Odoo repositories.")
|
|
53
|
+
raise typer.Exit(1)
|
|
54
|
+
|
|
55
|
+
venv_python = cfg.venv_path / "bin" / "python"
|
|
56
|
+
|
|
57
|
+
# Build command
|
|
58
|
+
cmd = [str(venv_python)]
|
|
59
|
+
|
|
60
|
+
if debug:
|
|
61
|
+
success("Debug mode enabled - attach VSCode to port 5678")
|
|
62
|
+
cmd.extend(["-m", "debugpy", "--listen", "0.0.0.0:5678", "--wait-for-client"])
|
|
63
|
+
|
|
64
|
+
cmd.extend([str(cfg.odoo_bin), "-c", str(cfg.config_file)])
|
|
65
|
+
|
|
66
|
+
if db_name:
|
|
67
|
+
cmd.extend(["-d", db_name])
|
|
68
|
+
|
|
69
|
+
if install:
|
|
70
|
+
cmd.extend(["-i", install])
|
|
71
|
+
|
|
72
|
+
if update:
|
|
73
|
+
cmd.extend(["-u", update])
|
|
74
|
+
|
|
75
|
+
if dev:
|
|
76
|
+
cmd.extend(["--dev", dev])
|
|
77
|
+
|
|
78
|
+
cmd.extend(["--http-port", str(port)])
|
|
79
|
+
|
|
80
|
+
success(f"Starting Odoo ({cfg.odoo_version}) on port {port}...")
|
|
81
|
+
if debug:
|
|
82
|
+
warning("Waiting for debugger to attach on port 5678...")
|
|
83
|
+
|
|
84
|
+
info(f"Config: {cfg.config_file}")
|
|
85
|
+
if db_name:
|
|
86
|
+
info(f"Database: {db_name}")
|
|
87
|
+
|
|
88
|
+
# Run Odoo, passing through signals
|
|
89
|
+
try:
|
|
90
|
+
process = subprocess.Popen(cmd, cwd=cfg.project_dir)
|
|
91
|
+
|
|
92
|
+
def signal_handler(signum, frame):
|
|
93
|
+
process.send_signal(signum)
|
|
94
|
+
|
|
95
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
96
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
97
|
+
|
|
98
|
+
sys.exit(process.wait())
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
process.terminate()
|
|
101
|
+
process.wait()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def shell(
|
|
105
|
+
db_name: Annotated[str, typer.Argument(help="Database name")],
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Open a local Odoo shell with database context."""
|
|
108
|
+
cfg = load_config()
|
|
109
|
+
|
|
110
|
+
# Verify setup
|
|
111
|
+
if not cfg.venv_path.exists():
|
|
112
|
+
error(f"Virtual environment not found at {cfg.venv_path}")
|
|
113
|
+
error("Run 'odoo-dev setup' first.")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
venv_python = cfg.venv_path / "bin" / "python"
|
|
117
|
+
|
|
118
|
+
success(f"Opening Odoo shell with database {db_name}...")
|
|
119
|
+
|
|
120
|
+
cmd = [
|
|
121
|
+
str(venv_python),
|
|
122
|
+
str(cfg.odoo_bin),
|
|
123
|
+
"shell",
|
|
124
|
+
"-d",
|
|
125
|
+
db_name,
|
|
126
|
+
"-c",
|
|
127
|
+
str(cfg.config_file),
|
|
128
|
+
"--no-http",
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
os.execv(str(venv_python), cmd)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def update(
|
|
135
|
+
modules: Annotated[
|
|
136
|
+
str, typer.Argument(help="Modules to update (comma-separated)")
|
|
137
|
+
] = "all",
|
|
138
|
+
db_name: Annotated[
|
|
139
|
+
str, typer.Option("-d", "--database", help="Database name")
|
|
140
|
+
] = "odoo",
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Update specified modules."""
|
|
143
|
+
cfg = load_config()
|
|
144
|
+
|
|
145
|
+
if not cfg.venv_path.exists():
|
|
146
|
+
error(f"Virtual environment not found at {cfg.venv_path}")
|
|
147
|
+
error("Run 'odoo-dev setup' first.")
|
|
148
|
+
raise typer.Exit(1)
|
|
149
|
+
|
|
150
|
+
venv_python = cfg.venv_path / "bin" / "python"
|
|
151
|
+
|
|
152
|
+
success(f"Updating modules: {modules} on database {db_name}...")
|
|
153
|
+
|
|
154
|
+
result = subprocess.run(
|
|
155
|
+
[
|
|
156
|
+
str(venv_python),
|
|
157
|
+
str(cfg.odoo_bin),
|
|
158
|
+
"-c",
|
|
159
|
+
str(cfg.config_file),
|
|
160
|
+
"-d",
|
|
161
|
+
db_name,
|
|
162
|
+
"-u",
|
|
163
|
+
modules,
|
|
164
|
+
"--stop-after-init",
|
|
165
|
+
],
|
|
166
|
+
cwd=cfg.project_dir,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if result.returncode == 0:
|
|
170
|
+
success("Module update complete!")
|
|
171
|
+
else:
|
|
172
|
+
error(f"Module update failed with exit code {result.returncode}")
|
|
173
|
+
raise typer.Exit(result.returncode)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test(
|
|
177
|
+
modules: Annotated[
|
|
178
|
+
str | None, typer.Argument(help="Modules to test (comma-separated)")
|
|
179
|
+
] = None,
|
|
180
|
+
db_name: Annotated[
|
|
181
|
+
str | None, typer.Option("-d", "--database", help="Test database name")
|
|
182
|
+
] = None,
|
|
183
|
+
test_tags: Annotated[
|
|
184
|
+
str | None, typer.Option("--test-tags", help="Test tags to filter")
|
|
185
|
+
] = None,
|
|
186
|
+
coverage: Annotated[
|
|
187
|
+
bool, typer.Option("--coverage/--no-coverage", help="Enable coverage reporting")
|
|
188
|
+
] = True,
|
|
189
|
+
keep_db: Annotated[
|
|
190
|
+
bool, typer.Option("--keep-db", help="Keep test database after tests")
|
|
191
|
+
] = False,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Run tests for specified modules."""
|
|
194
|
+
import time
|
|
195
|
+
|
|
196
|
+
cfg = load_config()
|
|
197
|
+
|
|
198
|
+
# Verify setup
|
|
199
|
+
if not cfg.venv_path.exists():
|
|
200
|
+
error(f"Virtual environment not found at {cfg.venv_path}")
|
|
201
|
+
error("Run 'odoo-dev setup' first.")
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
if not cfg.config_file.exists():
|
|
205
|
+
error(f"Config file not found: {cfg.config_file}")
|
|
206
|
+
error("Run 'odoo-dev setup' first.")
|
|
207
|
+
raise typer.Exit(1)
|
|
208
|
+
|
|
209
|
+
# Generate test database name if not provided
|
|
210
|
+
if db_name is None:
|
|
211
|
+
db_name = f"test_{int(time.time())}"
|
|
212
|
+
|
|
213
|
+
venv_python = cfg.venv_path / "bin" / "python"
|
|
214
|
+
|
|
215
|
+
# Determine modules to test
|
|
216
|
+
if modules is None:
|
|
217
|
+
# Use manifestoo to list addons in the addons directory
|
|
218
|
+
result = subprocess.run(
|
|
219
|
+
[
|
|
220
|
+
str(venv_python),
|
|
221
|
+
"-m",
|
|
222
|
+
"manifestoo",
|
|
223
|
+
"--select-addons-dir",
|
|
224
|
+
str(cfg.addons_dir),
|
|
225
|
+
"list",
|
|
226
|
+
"--separator=,",
|
|
227
|
+
],
|
|
228
|
+
capture_output=True,
|
|
229
|
+
text=True,
|
|
230
|
+
env={**os.environ, "ODOO_VERSION": "", "ODOO_SERIES": ""},
|
|
231
|
+
)
|
|
232
|
+
modules = result.stdout.strip()
|
|
233
|
+
if not modules:
|
|
234
|
+
error(f"No addons found in {cfg.addons_dir}")
|
|
235
|
+
raise typer.Exit(1)
|
|
236
|
+
|
|
237
|
+
# Get full addons path from config
|
|
238
|
+
addons_path = _get_addons_path(cfg.config_file)
|
|
239
|
+
|
|
240
|
+
# Calculate dependencies with manifestoo
|
|
241
|
+
success("Calculating dependencies...")
|
|
242
|
+
result = subprocess.run(
|
|
243
|
+
[
|
|
244
|
+
str(venv_python),
|
|
245
|
+
"-m",
|
|
246
|
+
"manifestoo",
|
|
247
|
+
"--addons-path",
|
|
248
|
+
addons_path,
|
|
249
|
+
f"--select-include={modules}",
|
|
250
|
+
"list-depends",
|
|
251
|
+
"--separator=,",
|
|
252
|
+
"--transitive",
|
|
253
|
+
],
|
|
254
|
+
capture_output=True,
|
|
255
|
+
text=True,
|
|
256
|
+
env={**os.environ, "ODOO_VERSION": "", "ODOO_SERIES": ""},
|
|
257
|
+
)
|
|
258
|
+
deps = result.stdout.strip() or "base"
|
|
259
|
+
|
|
260
|
+
# Find available port
|
|
261
|
+
http_port = _find_available_port(8069)
|
|
262
|
+
|
|
263
|
+
success("=== Test Run ===")
|
|
264
|
+
info(f"Database: {db_name}")
|
|
265
|
+
info(f"HTTP Port: {http_port}")
|
|
266
|
+
info(f"Modules: {modules}")
|
|
267
|
+
info(f"Dependencies: {deps}")
|
|
268
|
+
|
|
269
|
+
# Step 1: Install dependencies without tests
|
|
270
|
+
success("Installing dependencies...")
|
|
271
|
+
result = subprocess.run(
|
|
272
|
+
[
|
|
273
|
+
str(venv_python),
|
|
274
|
+
str(cfg.odoo_bin),
|
|
275
|
+
"-c",
|
|
276
|
+
str(cfg.config_file),
|
|
277
|
+
"-d",
|
|
278
|
+
db_name,
|
|
279
|
+
"-i",
|
|
280
|
+
deps,
|
|
281
|
+
"--stop-after-init",
|
|
282
|
+
"--http-port",
|
|
283
|
+
str(http_port),
|
|
284
|
+
],
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if result.returncode != 0:
|
|
288
|
+
error("Failed to install dependencies")
|
|
289
|
+
raise typer.Exit(1)
|
|
290
|
+
|
|
291
|
+
# Step 2: Run tests
|
|
292
|
+
success(f"Running tests: {modules}")
|
|
293
|
+
|
|
294
|
+
if coverage:
|
|
295
|
+
# Build coverage source paths
|
|
296
|
+
coverage_source = ",".join(str(cfg.addons_dir / m) for m in modules.split(","))
|
|
297
|
+
|
|
298
|
+
test_cmd = [
|
|
299
|
+
str(venv_python),
|
|
300
|
+
"-m",
|
|
301
|
+
"coverage",
|
|
302
|
+
"run",
|
|
303
|
+
f"--source={coverage_source}",
|
|
304
|
+
str(cfg.odoo_bin),
|
|
305
|
+
]
|
|
306
|
+
else:
|
|
307
|
+
test_cmd = [str(venv_python), str(cfg.odoo_bin)]
|
|
308
|
+
|
|
309
|
+
test_cmd.extend(
|
|
310
|
+
[
|
|
311
|
+
"-c",
|
|
312
|
+
str(cfg.config_file),
|
|
313
|
+
"-d",
|
|
314
|
+
db_name,
|
|
315
|
+
"-i",
|
|
316
|
+
modules,
|
|
317
|
+
"-u",
|
|
318
|
+
modules,
|
|
319
|
+
"--test-enable",
|
|
320
|
+
"--stop-after-init",
|
|
321
|
+
"--http-port",
|
|
322
|
+
str(http_port),
|
|
323
|
+
]
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if test_tags:
|
|
327
|
+
test_cmd.extend(["--test-tags", test_tags])
|
|
328
|
+
|
|
329
|
+
result = subprocess.run(test_cmd)
|
|
330
|
+
test_exit_code = result.returncode
|
|
331
|
+
|
|
332
|
+
# Generate coverage report
|
|
333
|
+
if coverage:
|
|
334
|
+
success("Coverage report:")
|
|
335
|
+
subprocess.run([str(venv_python), "-m", "coverage", "report"])
|
|
336
|
+
subprocess.run(
|
|
337
|
+
[
|
|
338
|
+
str(venv_python),
|
|
339
|
+
"-m",
|
|
340
|
+
"coverage",
|
|
341
|
+
"html",
|
|
342
|
+
"-d",
|
|
343
|
+
str(cfg.project_dir / "coverage_html"),
|
|
344
|
+
]
|
|
345
|
+
)
|
|
346
|
+
info(f"HTML report: {cfg.project_dir / 'coverage_html' / 'index.html'}")
|
|
347
|
+
|
|
348
|
+
# Clean up test database
|
|
349
|
+
if not keep_db:
|
|
350
|
+
success(f"Cleaning up: {db_name}")
|
|
351
|
+
subprocess.run(
|
|
352
|
+
[
|
|
353
|
+
str(venv_python),
|
|
354
|
+
str(cfg.odoo_bin),
|
|
355
|
+
"db",
|
|
356
|
+
"-c",
|
|
357
|
+
str(cfg.config_file),
|
|
358
|
+
"drop",
|
|
359
|
+
db_name,
|
|
360
|
+
],
|
|
361
|
+
capture_output=True,
|
|
362
|
+
)
|
|
363
|
+
else:
|
|
364
|
+
info(f"Keeping test database: {db_name}")
|
|
365
|
+
|
|
366
|
+
if test_exit_code == 0:
|
|
367
|
+
success("=== All tests passed! ===")
|
|
368
|
+
else:
|
|
369
|
+
error(f"=== Tests failed (exit code: {test_exit_code}) ===")
|
|
370
|
+
raise typer.Exit(test_exit_code)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def scaffold(
|
|
374
|
+
module_name: Annotated[str, typer.Argument(help="Name for the new module")],
|
|
375
|
+
dest: Annotated[
|
|
376
|
+
str | None,
|
|
377
|
+
typer.Option("-d", "--dest", help="Destination directory (default: addons/)"),
|
|
378
|
+
] = None,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""Create a new Odoo module from template."""
|
|
381
|
+
cfg = load_config()
|
|
382
|
+
|
|
383
|
+
if not cfg.venv_path.exists():
|
|
384
|
+
error(f"Virtual environment not found at {cfg.venv_path}")
|
|
385
|
+
error("Run 'odoo-dev setup' first.")
|
|
386
|
+
raise typer.Exit(1)
|
|
387
|
+
|
|
388
|
+
venv_python = cfg.venv_path / "bin" / "python"
|
|
389
|
+
destination = dest or str(cfg.addons_dir)
|
|
390
|
+
|
|
391
|
+
success(f"Creating module '{module_name}' in {destination}...")
|
|
392
|
+
|
|
393
|
+
result = subprocess.run(
|
|
394
|
+
[
|
|
395
|
+
str(venv_python),
|
|
396
|
+
str(cfg.odoo_bin),
|
|
397
|
+
"scaffold",
|
|
398
|
+
module_name,
|
|
399
|
+
destination,
|
|
400
|
+
],
|
|
401
|
+
cwd=cfg.project_dir,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if result.returncode == 0:
|
|
405
|
+
success(f"Module '{module_name}' created successfully!")
|
|
406
|
+
else:
|
|
407
|
+
error("Failed to create module")
|
|
408
|
+
raise typer.Exit(result.returncode)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _get_addons_path(config_file) -> str:
|
|
412
|
+
"""Extract addons_path from odoo.conf."""
|
|
413
|
+
for line in config_file.read_text().splitlines():
|
|
414
|
+
if line.strip().startswith("addons_path"):
|
|
415
|
+
return line.split("=", 1)[1].strip()
|
|
416
|
+
return ""
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _find_available_port(start: int = 8069) -> int:
|
|
420
|
+
"""Find an available port starting from the given port."""
|
|
421
|
+
import socket
|
|
422
|
+
|
|
423
|
+
for port in range(start, start + 100):
|
|
424
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
425
|
+
try:
|
|
426
|
+
s.bind(("127.0.0.1", port))
|
|
427
|
+
return port
|
|
428
|
+
except OSError:
|
|
429
|
+
continue
|
|
430
|
+
import random
|
|
431
|
+
|
|
432
|
+
return random.randint(49152, 65535)
|