Skip to content

Commit e6a7b56

Browse files
committed
imp: project
1 parent 6740b46 commit e6a7b56

24 files changed

Lines changed: 456 additions & 191 deletions

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,24 @@
5555

5656
It serves as a starting point for development teams to build, test, and customize their isOdoo-based applications. It facilitates local development, testing environments, and the creation of optimized container images ready for production deployment.
5757

58+
## Environment Variables (dev mode)
59+
60+
| Name | Description | Default |
61+
| ---- | ----------- | ------- |
62+
| DEBUGPY_ENABLED | Enable debugpy | false |
63+
| ODOO_DEV_MODES | Development modes | all |
64+
65+
** Check available "dev modes" here: https://www.odoo.com/documentation/19.0/developer/reference/cli.html#cmdoption-odoo-bin-dev
66+
5867
## Simple Usage
5968

6069
```sh
6170
cruft create https://github.com/GrupoIsonor/isodoo-template.git
71+
cd <project_folder>
72+
inv db init
73+
docker compose up
6274
```
6375

64-
Due to security policies, this template does not prompt for passwords. Default values are used. The best way to control these values is through the python API or by forcing the values in the Cookiecutter configuration file.
65-
66-
Default values:
67-
- Postgres Superuser Password: postgres
68-
- Odoo DB Password: odoo
69-
- Odoo Admin DB Manager Password: superadmin
70-
71-
Cookiecutter keys:
72-
- Postgres Superuser Password: _default_postgres_superuser_password
73-
- Odoo DB Password: _default_odoo_db_password
74-
- Odoo Admin DB Manager Password:: _default_odoo_admin_passwd
75-
7676
## Advance Usage
7777

7878
The best way to have full control is to fork this project and customize it (Don't forget to share anything you find interesting!)

cookiecutter.json

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
{
22
"project_name": "My isOdoo Project",
3-
"project_slug": "my-isodoo-project",
3+
"project_slug": "{{ cookiecutter.project_name | lower | replace(' ', '_') }}",
44
"project_owner": "Grupo Isonor",
55
"project_owner_url": "https://www.grupoisonor.es",
66
"project_license": ["AGPL-3", "GPL-2", "GPL-2 or any later version", "GPL-3", "GPL-3 or any later version", "LGPL-3", "Proprietary"],
7-
"project_repository": "",
7+
"project_repository": "{{ 'https://www.gitlab.com/' ~ cookiecutter.project_owner | replace(' ', '') ~ cookiecutter.project_slug }}",
88
"odoo_version": "19.0",
99
"odoo_db_name": "odoodb",
1010
"odoo_db_user": "odoo",
11+
"odoo_db_password": "odoo",
1112
"odoo_db_filter": "^{{ cookiecutter.odoo_db_name }}$",
13+
"odoo_admin_passwd": "superadmin",
14+
"odoo_base_language": "{{ get_odoo_lang('en_US') }}",
1215
"odoo_dependencies_apt": "",
1316
"odoo_dependencies_npm": "",
1417
"odoo_dependencies_pip": "",
15-
"postgres_version": "18",
18+
"postgres_version": "{{ get_odoo_pg_version(cookiecutter.odoo_version) }}",
1619
"postgres_superuser_user": "postgres",
17-
"squid_allowed_domains": ".google.com,.googleapis.com,.cdnjs.cloudflare.com,.gravatar.com,.github.com",
20+
"postgres_superuser_password": "postgres",
21+
"squid_allowed_domains": ".unsplash.com,.google.com,.googleapis.com,.cdnjs.cloudflare.com,.gravatar.com,.github.com",
1822
"squid_allowed_ips": "",
1923
"squid_allowed_ports": "80,443",
2024
"greenmail_users": "admin:admin@example.com,demo:demo@example.com",
21-
"_default_odoo_db_password": "odoo",
22-
"_default_odoo_admin_passwd": "superadmin",
23-
"_default_postgres_superuser_password": "postgres",
24-
"_default_backup_password": "super_secret_backup_password",
2525
"_precommit_node_version": "24.13.0",
2626
"_precommit_maintainer_tools_rev": "71aa4caec15e8c1456b4da19e9f39aa0aa7377a9",
2727
"_precommit_odoo_hooks_rev": "v0.2.20",
@@ -32,15 +32,18 @@
3232
"_precommit_eslint_rev": "v9.39.1",
3333
"_debugpy_port": "5678",
3434
"_squid_debug_options": "ALL,1 28,3 33,2",
35+
"_squid_no_proxy": "localhost,127.0.0.1,::1,*.local,10.0.0.0/8,192.168.0.0/16",
36+
"_bind_ip": "127.0.0.1",
3537

3638
"__use_squid_allow": "{{ cookiecutter.squid_allowed_domains | length > 0 or cookiecutter.squid_allowed_ips | length > 0 or cookiecutter.squid_allowed_ports | length > 0 }}",
39+
"__postgres_pgdata": "{{ '/var/lib/postgresql' if cookiecutter.postgres_version|float >= 18 else '/var/lib/postgresql/data' }}",
3740

3841
"_copy_without_render": [
3942
".module-readme.rst.j2"
4043
],
4144

4245
"_extensions": [
43-
"local_extensions.CPUExtension",
46+
"local_extensions.isOdooAutoConfig",
4447
"jinja2_ansible_filters.AnsibleCoreFiltersExtension"
4548
]
4649
}

hooks/post_gen_project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@
3535
shutil.rmtree(project_root / "macros")
3636

3737
### Prepare Dev Environment
38-
subprocess.run(["inv", "mode", "dev"], check=True)
38+
subprocess.run(["inv", "mode", "dev"], check=True, capture_output=True)

local_extensions/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/usr/bin/env python3
22
# Copyright Grupo Isonor - Alexandre D. <dev@redneboa.es>
3-
from .hardware import CPUExtension
3+
from .isodoo import isOdooAutoConfig

local_extensions/hardware.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

local_extensions/isodoo.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python3
2+
# Copyright Grupo Isonor - Alexandre D. <dev@redneboa.es>
3+
import locale
4+
from jinja2.ext import Extension
5+
6+
PG_VERSIONS = {
7+
"6.0": "9.6",
8+
"6.1": "9.6",
9+
"7.0": "9.6",
10+
"8.0": "9.6",
11+
"9.0": "11",
12+
"10.0": "12",
13+
"11.0": "13",
14+
"12.0": "14",
15+
"13.0": "15",
16+
"14.0": "16",
17+
"15.0": "16",
18+
"16.0": "16",
19+
"17.0": "17",
20+
"18.0": "17",
21+
"19.0": "18",
22+
}
23+
24+
class isOdooAutoConfig(Extension):
25+
def __init__(self, environment):
26+
super().__init__(environment)
27+
environment.globals.update(
28+
get_odoo_pg_version=self.getPGVersion,
29+
get_odoo_lang=self.getLang,
30+
)
31+
32+
def getPGVersion(self, odoo_version):
33+
return PG_VERSIONS.get(odoo_version)
34+
35+
def getLang(self, default_lang):
36+
return locale.getlocale()[0] or default_lang

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ dependencies = [
99
"invoke>=3.0.3",
1010
"jinja2-ansible-filters>=1.3.2",
1111
"pytest>=9.0.3",
12+
"pyyaml>=6.0.3",
1213
]

tests/conftest.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,33 @@
66
import sys
77
import os
88
import shutil
9+
import time
10+
import requests
911
from io import StringIO
1012
from invoke import Context, Collection, Config
1113
import contextlib
1214
import pytest
15+
import yaml
1316
from cookiecutter.main import cookiecutter
1417

18+
EXTRA_ADDONS = {
19+
"6.0": ("l10n-spain", "city"),
20+
"6.1": ("reporting-engine", "report_xls"),
21+
"7.0": ("reporting-engine", "report_xls"),
22+
"8.0": ("reporting-engine", "report_xlsx"),
23+
"9.0": ("reporting-engine", "report_xlsx"),
24+
"10.0": ("reporting-engine", "report_xlsx"),
25+
"11.0": ("reporting-engine", "report_xlsx"),
26+
"12.0": ("reporting-engine", "report_xlsx"),
27+
"13.0": ("reporting-engine", "report_xlsx_boilerplate"),
28+
"14.0": ("reporting-engine", "report_fillpdf"),
29+
"15.0": ("reporting-engine", "report_xlsx"),
30+
"16.0": ("reporting-engine", "sql_export_excel"),
31+
"17.0": ("reporting-engine", "sql_export_excel"),
32+
"18.0": ("reporting-engine", "sql_export_excel"),
33+
"19.0": ("reporting-engine", "report_xlsx"),
34+
}
35+
1536

1637
def _get_preferred_client_type():
1738
if shutil.which("podman"):
@@ -24,6 +45,21 @@ def switch_project_mode(container_engine: str, project_path: str | Path, mode:
2445
invoke_task(container_engine, project_path, "down")
2546
return invoke_task(container_engine, project_path, "mode", mode)
2647

48+
def wait_for_odoo(ip_address, port):
49+
from requests.exceptions import RequestException
50+
51+
url = f"http://{ip_address}:{port}"
52+
for _ in range(60):
53+
try:
54+
r = requests.get(url, timeout=5)
55+
if r.status_code == 200:
56+
break
57+
except RequestException:
58+
pass
59+
time.sleep(2)
60+
else:
61+
raise TimeoutError("Odoo did not start on time")
62+
2763
def invoke_task(container_engine: str, project_path: str | Path, task_name: str, *args, **kwargs):
2864
project_path = Path(project_path).resolve()
2965
tasks_path = project_path / "tasks.py"
@@ -53,6 +89,10 @@ def invoke_task(container_engine: str, project_path: str | Path, task_name: str,
5389
ctx.config.run.warn = True
5490
ctx.config.run.in_stream = False
5591
ctx.config["isodoo_container_engine"] = container_engine
92+
invoke_env = kwargs.get("invoke_env")
93+
if invoke_env:
94+
ctx.config.run.env.update(invoke_env)
95+
kwargs.pop("invoke_env")
5696
with contextlib.redirect_stdout(stdout_capture), contextlib.redirect_stderr(stderr_capture):
5797
result = task(ctx, *args, **kwargs)
5898
return {
@@ -79,6 +119,9 @@ def env_info(pytestconfig):
79119
"ip": "127.0.0.1",
80120
"ports": {
81121
"odoo": f"{odoo_ver_int}069",
122+
"pgweb": f"{odoo_ver_int}081",
123+
"roundcube": f"{odoo_ver_int}090",
124+
"debugpy": "5678",
82125
},
83126
"options": {
84127
"odoo_version": odoo_ver,
@@ -101,11 +144,19 @@ def project_tmpl(env_info, tmp_path_factory):
101144
)
102145
project_path = Path(result_dir)
103146
assert project_path.exists(), "The template has not been rendered"
147+
with open(project_path / "addons" / "addons.yaml", "r+") as f:
148+
data = yaml.safe_load(f) or {}
149+
extra_addon_repo, extra_addon_name = EXTRA_ADDONS[env_info["options"]["odoo_version"]]
150+
data[extra_addon_repo] = [extra_addon_name]
151+
yaml.dump(data, f, sort_keys=False, default_flow_style=False)
104152
try:
105-
# Initialize Odoo
153+
# Use CI Mode
106154
switch_project_mode(env_info["client_type"], project_path, "ci")
107-
invoke_task(env_info["client_type"], project_path, "module", "install", "base")
155+
# Build
156+
invoke_task(env_info["client_type"], project_path, "build", no_cache=True)
157+
# Initialize Odoo
158+
invoke_task(env_info["client_type"], project_path, "db", "init")
108159
yield project_path
109160
finally:
110161
switch_project_mode(env_info["client_type"], project_path, "dev")
111-
invoke_task(env_info["client_type"], project_path, "down", remove_volumes=True)
162+
invoke_task(env_info["client_type"], project_path, "destroy-this-project", force=True)

tests/test_project.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,55 @@
11
#!/usr/bin/env python3
22
# Copyright Grupo Isonor - Alexandre D. <dev@redneboa.es>
33

4+
import time
5+
import socket
6+
import json
7+
import pytest
8+
from conftest import invoke_task, switch_project_mode
9+
410

511
def test_project_structure(project_tmpl):
612
assert (project_tmpl / "addons" / "git").is_dir()
713
assert (project_tmpl / "compose.yaml").is_file()
814
assert not (project_tmpl / "macros").exists()
915
assert not (project_tmpl / "recipes").exists()
1016
assert not (project_tmpl / "_helpers.jinja").exists()
17+
18+
def test_debugpy(project_tmpl, env_info):
19+
switch_project_mode(env_info["client_type"], project_tmpl, "dev")
20+
invoke_task(env_info["client_type"], project_tmpl, "build", mode="dev")
21+
# Up
22+
invoke_task(env_info["client_type"], project_tmpl, "up", detach=True, invoke_env={'DEBUGPY_ENABLED': 'true'})
23+
# Test Basic Connection
24+
host = env_info['ip']
25+
port = int(env_info['ports']['debugpy'])
26+
for _ in range(30):
27+
try:
28+
with socket.create_connection((host, port), timeout=3) as s:
29+
req = {
30+
"seq": 1,
31+
"type": "request",
32+
"command": "initialize",
33+
"arguments": {"adapterID": "python"}
34+
}
35+
msg = json.dumps(req)
36+
s.sendall(f"Content-Length: {len(msg)}\r\n\r\n{msg}".encode())
37+
response = s.recv(8192).decode(errors='ignore')
38+
if any(x in response for x in ["debugpySockets", "success", "output"]):
39+
break
40+
except:
41+
time.sleep(2)
42+
else:
43+
pytest.fail("debugpy is not working")
44+
# Down
45+
invoke_task(env_info["client_type"], project_tmpl, "down")
46+
# Restore Image
47+
switch_project_mode(env_info["client_type"], project_tmpl, "ci")
48+
invoke_task(env_info["client_type"], project_tmpl, "build")
49+
50+
def test_squid(project_tmpl, env_info):
51+
switch_project_mode(env_info["client_type"], project_tmpl, "dev")
52+
result = invoke_task(env_info["client_type"], project_tmpl, "check-connection-code", dst="http://www.amazon.com")
53+
assert "403" in result["return"]
54+
result = invoke_task(env_info["client_type"], project_tmpl, "check-connection-code", dst="http://www.google.com")
55+
assert "200" in result["return"]

0 commit comments

Comments
 (0)