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.
@@ -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)