├── odoo ├── custom │ ├── ssh │ │ ├── id_rsa │ │ ├── id_rsa.pub │ │ ├── config │ │ └── known_hosts │ ├── build.d │ │ └── .empty │ ├── conf.d │ │ ├── .empty │ │ ├── 50-queue.conf │ │ └── 00-defaults.conf │ ├── dependencies │ │ ├── apt.txt │ │ ├── gem.txt │ │ ├── apt_build.txt │ │ ├── npm.txt │ │ └── pip.txt │ ├── entrypoint.d │ │ └── .empty │ ├── .DS_Store │ └── src │ │ ├── .DS_Store │ │ ├── addons.yaml │ │ ├── private │ │ └── .editorconfig │ │ └── repos.yaml ├── .DS_Store ├── Dockerfile └── .dockerignore ├── requirements.txt ├── .prettierrc.yml ├── .editorconfig ├── .flake8 ├── .isort.cfg ├── demo.yaml ├── .gitignore ├── prod.yaml ├── .copier-answers.yml ├── setup-devel.yaml ├── common.yaml ├── test.yaml ├── .pylintrc-mandatory ├── .pylintrc ├── .pre-commit-config.yaml ├── .eslintrc.yml ├── devel.yaml ├── README.md ├── LICENSE └── tasks.py /odoo/custom/ssh/id_rsa: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo/custom/build.d/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo/custom/conf.d/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo/custom/ssh/id_rsa.pub: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo/custom/dependencies/apt.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo/custom/dependencies/gem.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo/custom/entrypoint.d/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo/custom/dependencies/apt_build.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odoo/custom/dependencies/npm.txt: -------------------------------------------------------------------------------- 1 | rtlcss 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | invoke 2 | pre-commit 3 | git-aggregator 4 | -------------------------------------------------------------------------------- /odoo/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSPP/openspp-docker/HEAD/odoo/.DS_Store -------------------------------------------------------------------------------- /odoo/custom/ssh/config: -------------------------------------------------------------------------------- 1 | # See syntax in https://www.ssh.com/ssh/config/ and `man ssh_config` 2 | -------------------------------------------------------------------------------- /odoo/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ODOO_VERSION=17.0 2 | FROM ghcr.io/tecnativa/doodba:${ODOO_VERSION}-onbuild 3 | -------------------------------------------------------------------------------- /odoo/custom/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSPP/openspp-docker/HEAD/odoo/custom/.DS_Store -------------------------------------------------------------------------------- /odoo/custom/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSPP/openspp-docker/HEAD/odoo/custom/src/.DS_Store -------------------------------------------------------------------------------- /odoo/.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore all src directories except private and files with a dot 2 | custom/src/* 3 | !custom/src/private 4 | !custom/src/*.* 5 | auto 6 | -------------------------------------------------------------------------------- /odoo/custom/conf.d/50-queue.conf: -------------------------------------------------------------------------------- 1 | [queue_job] 2 | channels = root:2 3 | jobrunner_db_host = db 4 | jobrunner_db_port = 5432 5 | jobrunner_db_user = odoo 6 | jobrunner_db_password = odoopassword 7 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # Defaults for all prettier-supported languages 2 | # Prettier will complete this with settings from .editorconfig file. 3 | bracketSpacing: false 4 | printWidth: 88 5 | proseWrap: always 6 | semi: true 7 | trailingComma: "es5" 8 | -------------------------------------------------------------------------------- /odoo/custom/conf.d/00-defaults.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | [options] 4 | limit_memory_soft = 1048576000 5 | limit_time_cpu = 600 6 | limit_time_real = 600 7 | limit_time_real_cron = 600 8 | db_name = 9 | workers = 6 10 | server_wide_modules = base,web,queue_job 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{code-snippets,code-workspace,json,md,yaml,yml}{,.jinja}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | max-complexity = 16 4 | # B = bugbear 5 | # B9 = bugbear opinionated (incl line length) 6 | select = C,E,F,W,B,B9 7 | # E203: whitespace before ':' (black behaviour) 8 | # E501: flake8 line length (covered by bugbear B950) 9 | # W503: line break before binary operator (black behaviour) 10 | ignore = E203,E501,W503 11 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | ; see https://github.com/psf/black 3 | multi_line_output=3 4 | include_trailing_comma=True 5 | force_grid_wrap=0 6 | combine_as_imports=True 7 | use_parentheses=True 8 | line_length=88 9 | known_odoo=odoo 10 | known_odoo_addons=odoo.addons 11 | sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER 12 | default_section=THIRDPARTY 13 | ensure_newline_before_comments = True 14 | -------------------------------------------------------------------------------- /odoo/custom/src/addons.yaml: -------------------------------------------------------------------------------- 1 | openg2p_registry: 2 | - "*" 3 | openg2p_program: 4 | - "*" 5 | openg2p_rest_framework: 6 | - "fastapi" 7 | - "extendable" 8 | - "extendable_fastapi" 9 | openspp_modules: 10 | - "*" 11 | server-tools: 12 | - base_multi_image 13 | server-ux: 14 | - mass_editing 15 | queue: 16 | - queue_job 17 | muk_addons: 18 | - "*" 19 | server-backend: 20 | - "*" 21 | web-api: 22 | - endpoint_route_handler 23 | -------------------------------------------------------------------------------- /demo.yaml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | metabase: 5 | image: metabase/metabase:latest 6 | ports: 7 | - "3000:3000" 8 | depends_on: 9 | - db 10 | environment: 11 | MB_DB_TYPE: h2 12 | MB_DB_FILE: /data/metabase.db 13 | volumes: 14 | - metabase-data:/data 15 | 16 | networks: 17 | default: 18 | internal: ${DOODBA_NETWORK_INTERNAL-false} 19 | public: 20 | 21 | volumes: 22 | metabase-data: 23 | -------------------------------------------------------------------------------- /odoo/custom/src/private/.editorconfig: -------------------------------------------------------------------------------- 1 | # Configuration for known file extensions 2 | [*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml}] 3 | charset = utf-8 4 | end_of_line = lf 5 | indent_size = 4 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{json,yml,yaml,rst,md}] 11 | indent_size = 2 12 | 13 | # Do not configure editor for libs and autogenerated content 14 | [{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] 15 | charset = unset 16 | end_of_line = unset 17 | indent_size = unset 18 | indent_style = unset 19 | insert_final_newline = false 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all src directories except private and files with a dot 2 | /odoo/auto 3 | /odoo/custom/src/*/ 4 | !/odoo/custom/src/private/ 5 | 6 | # Ignore docker-compose.yml and overrides by default, to allow easy defaults per clone 7 | /docker-compose.yml 8 | /docker-compose.override.yml 9 | 10 | # Compiled formats, cache, temporary files, git garbage 11 | **.~ 12 | **.mo 13 | **.py[co] 14 | **.egg-info 15 | **.orig 16 | 17 | # User-specific or editor-specific development files and settings 18 | !/.vscode/*.jinja 19 | !/.vscode/extensions.json 20 | !/.vscode/launch.json 21 | !/.vscode/settings.json 22 | !/.vscode/tasks.json 23 | .vscode/* 24 | /odoo/custom/src/private/.vscode/* 25 | /.venv 26 | /doodba.*.code-workspace 27 | /src 28 | 29 | # Project-specific docker configurations 30 | /.docker/ 31 | 32 | .idea 33 | 34 | # OSX Files 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /odoo/custom/dependencies/pip.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/OCA/openupgradelib.git@master 2 | boto3>=1.35.12 3 | celery 4 | faker 5 | flower 6 | python-jose==3.3.0 7 | twilio>=9.2.4 8 | unicodecsv 9 | unidecode 10 | 11 | # REST API dependencies 12 | apispec>=4.0.0 13 | cachetools 14 | cerberus 15 | 16 | extendable 17 | extendable-pydantic==1.3.0 18 | jsondiff 19 | marshmallow 20 | marshmallow-objects>=2.0.0 21 | parse-accept-language 22 | pydantic 23 | pyquerystring 24 | schwifty 25 | typing-extensions 26 | 27 | # Geo dependencies 28 | geojson 29 | shapely 30 | simplejson 31 | pyproj 32 | 33 | # OpenSPP dependencies 34 | Pillow>=10.3.0 35 | PyLD 36 | bravado_core 37 | faker 38 | geojson 39 | jq 40 | jsonschema 41 | jwcrypto 42 | numpy>=1.22.2 43 | pyjwt>=2.4.0 44 | pyproj 45 | python-magic 46 | qrcode 47 | shapely 48 | simplejson 49 | swagger_spec_validator 50 | urllib3>=2.2.2 51 | xlrd 52 | zipp>=3.19.1 53 | openpyxl 54 | 55 | # fastapi dependencies 56 | fastapi==0.112.2 57 | python-multipart 58 | ujson 59 | a2wsgi 60 | parse-accept-language 61 | -------------------------------------------------------------------------------- /prod.yaml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | odoo: 5 | extends: 6 | file: common.yaml 7 | service: odoo 8 | restart: unless-stopped 9 | env_file: 10 | - .docker/odoo.env 11 | - .docker/db-access.env 12 | environment: 13 | DB_FILTER: "^prod" 14 | DOODBA_ENVIRONMENT: "${DOODBA_ENVIRONMENT-prod}" 15 | INITIAL_LANG: "en_US" 16 | depends_on: 17 | - db 18 | networks: 19 | default: 20 | labels: 21 | doodba.domain.main: "" 22 | 23 | db: 24 | extends: 25 | file: common.yaml 26 | service: db 27 | env_file: 28 | - .docker/db-creation.env 29 | restart: unless-stopped 30 | networks: 31 | default: 32 | inverseproxy_shared: 33 | labels: 34 | traefik.enable: "true" 35 | traefik.docker.network: "inverseproxy_shared" 36 | traefik.tcp.services.openspp-17-0-prod-database.loadbalancer.server.port: 5432 37 | 38 | networks: 39 | default: 40 | driver_opts: 41 | encrypted: 1 42 | 43 | volumes: 44 | filestore: 45 | db: 46 | -------------------------------------------------------------------------------- /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY 2 | _commit: v4.2.0 3 | _src_path: gh:Tecnativa/doodba-copier-template 4 | backup_deletion: false 5 | backup_dst: boto3+s3://example_bucket 6 | backup_email_from: contact@openspp.org 7 | backup_email_to: null 8 | backup_image_version: latest 9 | backup_tz: UTC 10 | cidr_whitelist: null 11 | domains_prod: 12 | - hosts: 13 | - demo.openspp.org 14 | domains_test: 15 | - hosts: 16 | - demo-test.openspp.org 17 | gitlab_url: null 18 | odoo_dbfilter: ^prod 19 | odoo_initial_lang: en_US 20 | odoo_listdb: false 21 | odoo_oci_image: openspp/openspp 22 | odoo_proxy: traefik 23 | odoo_version: 17.0 24 | paths_without_crawlers: 25 | - / 26 | postgres_cidr_whitelist: null 27 | postgres_dbname: prod 28 | postgres_exposed: false 29 | postgres_exposed_port: 5432 30 | postgres_username: odoo 31 | postgres_version: "15" 32 | project_author: OpenSPP 33 | project_license: LGPL-3.0-or-later 34 | project_name: OpenSPP 35 | smtp_canonical_default: null 36 | smtp_canonical_domains: null 37 | smtp_default_from: contact@openspp.org 38 | smtp_relay_host: null 39 | smtp_relay_port: 587 40 | smtp_relay_user: null 41 | smtp_relay_version: "10" 42 | -------------------------------------------------------------------------------- /setup-devel.yaml: -------------------------------------------------------------------------------- 1 | # Use this environment to download all repositories from `repos.yaml` file: 2 | # 3 | # export DOODBA_GITAGGREGATE_UID="$(id -u $USER)" DOODBA_GITAGGREGATE_GID="$(id -g $USER)" DOODBA_UMASK="$(umask)" 4 | # docker-compose -f setup-devel.yaml run --rm odoo 5 | # 6 | # You can clean your git project before if you want to have all really clean: 7 | # 8 | # git clean -ffd 9 | 10 | services: 11 | odoo: 12 | privileged: true 13 | build: 14 | context: ./odoo 15 | args: 16 | AGGREGATE: "false" 17 | DEPTH_DEFAULT: 100 18 | ODOO_VERSION: "17.0" 19 | ODOO_SOURCE: "odoo/odoo" 20 | PYTHONOPTIMIZE: "" 21 | PIP_INSTALL_ODOO: "false" 22 | CLEAN: "false" 23 | COMPILE: "false" 24 | UID: "${UID:-1000}" 25 | GID: "${GID:-1000}" 26 | networks: 27 | - public 28 | volumes: 29 | - ./odoo/custom/src:/opt/odoo/custom/src:z 30 | environment: 31 | DEPTH_DEFAULT: 100 32 | # XXX Export these variables before running setup to own the files 33 | UID: "${DOODBA_GITAGGREGATE_UID:-1000}" 34 | GID: "${DOODBA_GITAGGREGATE_GID:-1000}" 35 | UMASK: "$DOODBA_UMASK" 36 | user: root 37 | entrypoint: autoaggregate 38 | 39 | networks: 40 | public: 41 | -------------------------------------------------------------------------------- /common.yaml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | odoo: 5 | platform: linux/amd64 6 | build: 7 | context: ./odoo 8 | args: 9 | DB_VERSION: "15" 10 | ODOO_VERSION: "17.0" 11 | UID: "${UID:-1000}" 12 | GID: "${GID:-1000}" 13 | environment: 14 | EMAIL_FROM: "contact@openspp.org" 15 | PGDATABASE: &dbname prod 16 | PGUSER: &dbuser "odoo" 17 | PROXY_MODE: "false" 18 | LIST_DB: "true" 19 | tty: true 20 | volumes: 21 | - filestore:/var/lib/odoo:z 22 | 23 | db: 24 | platform: linux/amd64 25 | image: ghcr.io/openspp/postgis-autoconf:15-alpine 26 | shm_size: 4gb 27 | environment: 28 | POSTGRES_DB: *dbname 29 | POSTGRES_USER: *dbuser 30 | CONF_EXTRA: | 31 | work_mem = 512MB 32 | max_connections = 200 33 | shared_buffers = 1GB 34 | effective_cache_size = 3GB 35 | maintenance_work_mem = 256MB 36 | checkpoint_completion_target = 0.9 37 | wal_buffers = 16MB 38 | default_statistics_target = 100 39 | random_page_cost = 1.1 40 | effective_io_concurrency = 200 41 | work_mem = 2621kB 42 | huge_pages = off 43 | min_wal_size = 1GB 44 | max_wal_size = 4GB 45 | 46 | volumes: 47 | - db:/var/lib/postgresql/data:z 48 | 49 | smtpfake: 50 | platform: linux/amd64 51 | image: docker.io/mailhog/mailhog 52 | -------------------------------------------------------------------------------- /test.yaml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | odoo: 5 | extends: 6 | file: common.yaml 7 | service: odoo 8 | env_file: 9 | - .docker/odoo.env 10 | - .docker/db-access.env 11 | environment: 12 | DOODBA_ENVIRONMENT: "${DOODBA_ENVIRONMENT-test}" 13 | # To install demo data export DOODBA_WITHOUT_DEMO=false 14 | WITHOUT_DEMO: "${DOODBA_WITHOUT_DEMO-all}" 15 | SMTP_PORT: "1025" 16 | SMTP_SERVER: smtplocal 17 | restart: unless-stopped 18 | depends_on: 19 | - db 20 | - smtp 21 | networks: 22 | default: 23 | globalwhitelist_shared: 24 | labels: 25 | doodba.domain.main: "" 26 | command: 27 | - odoo 28 | - --workers=2 29 | - --max-cron-threads=1 30 | 31 | db: 32 | extends: 33 | file: common.yaml 34 | service: db 35 | env_file: 36 | - .docker/db-creation.env 37 | restart: unless-stopped 38 | 39 | smtp: 40 | extends: 41 | file: common.yaml 42 | service: smtpfake 43 | restart: unless-stopped 44 | networks: 45 | default: 46 | aliases: 47 | - smtplocal 48 | labels: 49 | doodba.domain.main: "" 50 | volumes: 51 | - "smtpconf:/etc/mailhog:ro,z" 52 | entrypoint: [sh, -c] 53 | command: 54 | - test -r /etc/mailhog/auth && export MH_AUTH_FILE=/etc/mailhog/auth; exec MailHog 55 | 56 | networks: 57 | default: 58 | internal: ${DOODBA_NETWORK_INTERNAL-true} 59 | driver_opts: 60 | encrypted: 1 61 | 62 | globalwhitelist_shared: 63 | external: true 64 | 65 | volumes: 66 | filestore: 67 | db: 68 | smtpconf: 69 | -------------------------------------------------------------------------------- /.pylintrc-mandatory: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | load-plugins=pylint_odoo 3 | score=n 4 | 5 | [ODOOLINT] 6 | readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst" 7 | manifest_required_keys=license 8 | manifest_deprecated_keys=active 9 | valid_odoo_versions=17.0 10 | 11 | [MESSAGES CONTROL] 12 | disable=all 13 | 14 | enable=anomalous-backslash-in-string, 15 | api-one-deprecated, 16 | api-one-multi-together, 17 | assignment-from-none, 18 | attribute-deprecated, 19 | class-camelcase, 20 | dangerous-default-value, 21 | dangerous-view-replace-wo-priority, 22 | duplicate-id-csv, 23 | duplicate-key, 24 | duplicate-xml-fields, 25 | duplicate-xml-record-id, 26 | eval-referenced, 27 | eval-used, 28 | incoherent-interpreter-exec-perm, 29 | manifest-author-string, 30 | manifest-deprecated-key, 31 | manifest-required-key, 32 | manifest-version-format, 33 | method-compute, 34 | method-inverse, 35 | method-required-super, 36 | method-search, 37 | missing-import-error, 38 | missing-manifest-dependency, 39 | openerp-exception-warning, 40 | pointless-statement, 41 | pointless-string-statement, 42 | print-used, 43 | redundant-keyword-arg, 44 | redundant-modulename-xml, 45 | reimported, 46 | relative-import, 47 | return-in-init, 48 | rst-syntax-error, 49 | sql-injection, 50 | too-few-format-args, 51 | translation-field, 52 | translation-required, 53 | unreachable, 54 | use-vim-comment, 55 | wrong-tabs-instead-of-spaces, 56 | xml-syntax-error 57 | 58 | [REPORTS] 59 | msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} 60 | output-format=colorized 61 | reports=no 62 | -------------------------------------------------------------------------------- /odoo/custom/ssh/known_hosts: -------------------------------------------------------------------------------- 1 | # Use `ssh-keyscan` to fill this file and ensure remote git hosts ssh keys 2 | 3 | # bitbucket.org:22 SSH-2.0-conker_8c537ded9a e1148f0abe57 4 | bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE= 5 | # bitbucket.org:22 SSH-2.0-conker_8c537ded9a 66c29d7ad5be 6 | bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M= 7 | # bitbucket.org:22 SSH-2.0-conker_8c537ded9a 551826d0fde9 8 | bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO 9 | 10 | # github.com:22 SSH-2.0-libssh-0.7.0 11 | github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk= 12 | 13 | # gitlab.com:22 SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2 14 | gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9 15 | # gitlab.com:22 SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2 16 | gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY= 17 | # gitlab.com:22 SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2 18 | gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf 19 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | load-plugins=pylint_odoo 3 | score=n 4 | 5 | [ODOOLINT] 6 | readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst" 7 | manifest_required_authors=OpenSPP 8 | manifest_required_keys=license 9 | manifest_deprecated_keys=description,active 10 | license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3,OPL-1,OEEL-1 11 | valid_odoo_versions=15.0 12 | 13 | [MESSAGES CONTROL] 14 | disable=all 15 | 16 | # This .pylintrc contains optional AND mandatory checks and is meant to be 17 | # loaded in an IDE to have it check everything, in the hope this will make 18 | # optional checks more visible to contributors who otherwise never look at a 19 | # green travis to see optional checks that failed. 20 | # .pylintrc-mandatory containing only mandatory checks is used the pre-commit 21 | # config as a blocking check. 22 | 23 | enable=anomalous-backslash-in-string, 24 | api-one-deprecated, 25 | api-one-multi-together, 26 | assignment-from-none, 27 | attribute-deprecated, 28 | class-camelcase, 29 | dangerous-default-value, 30 | dangerous-view-replace-wo-priority, 31 | duplicate-id-csv, 32 | duplicate-key, 33 | duplicate-xml-fields, 34 | duplicate-xml-record-id, 35 | eval-referenced, 36 | eval-used, 37 | incoherent-interpreter-exec-perm, 38 | license-allowed, 39 | manifest-author-string, 40 | manifest-deprecated-key, 41 | manifest-required-author, 42 | manifest-required-key, 43 | manifest-version-format, 44 | method-compute, 45 | method-inverse, 46 | method-required-super, 47 | method-search, 48 | missing-import-error, 49 | missing-manifest-dependency, 50 | openerp-exception-warning, 51 | pointless-statement, 52 | pointless-string-statement, 53 | print-used, 54 | redundant-keyword-arg, 55 | redundant-modulename-xml, 56 | reimported, 57 | relative-import, 58 | return-in-init, 59 | rst-syntax-error, 60 | sql-injection, 61 | too-few-format-args, 62 | translation-field, 63 | translation-required, 64 | unreachable, 65 | use-vim-comment, 66 | wrong-tabs-instead-of-spaces, 67 | xml-syntax-error, 68 | # messages that do not cause the lint step to fail 69 | consider-merging-classes-inherited, 70 | create-user-wo-reset-password, 71 | dangerous-filter-wo-user, 72 | deprecated-module, 73 | file-not-used, 74 | invalid-commit, 75 | missing-newline-extrafiles, 76 | missing-readme, 77 | no-utf8-coding-comment, 78 | odoo-addons-relative-import, 79 | old-api7-method-defined, 80 | redefined-builtin, 81 | too-complex, 82 | unnecessary-utf8-coding-comment 83 | 84 | [REPORTS] 85 | msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} 86 | output-format=colorized 87 | reports=no 88 | -------------------------------------------------------------------------------- /odoo/custom/src/repos.yaml: -------------------------------------------------------------------------------- 1 | # See https://github.com/Tecnativa/doodba#optodoocustomsrcreposyaml 2 | ./odoo: 3 | defaults: 4 | # Shallow repositories ($DEPTH_DEFAULT=1) are faster & thinner 5 | # You may need a bigger depth when merging PRs (use $DEPTH_MERGE 6 | # for a sane value of 100 commits) 7 | depth: $DEPTH_DEFAULT 8 | remotes: 9 | odoo: https://github.com/odoo/odoo.git 10 | openupgrade: https://github.com/OCA/OpenUpgrade.git 11 | target: odoo $ODOO_VERSION 12 | merges: 13 | - odoo $ODOO_VERSION 14 | # Example of a merge of the PR with the number 15 | # - oca refs/pull//head 16 | 17 | openg2p_registry: 18 | defaults: 19 | # Shallow repositories ($DEPTH_DEFAULT=1) are faster & thinner 20 | # You may need a bigger depth when merging PRs (use $DEPTH_MERGE 21 | # for a sane value of 100 commits) 22 | depth: $DEPTH_DEFAULT 23 | remotes: 24 | openg2p: https://github.com/OpenSPP/openg2p-registry.git 25 | target: openg2p $ODOO_VERSION 26 | merges: 27 | - openg2p 17.0-develop-openspp 28 | # Example of a merge of the PR with the number 29 | # - oca refs/pull//head 30 | 31 | openg2p_program: 32 | defaults: 33 | # Shallow repositories ($DEPTH_DEFAULT=1) are faster & thinner 34 | # You may need a bigger depth when merging PRs (use $DEPTH_MERGE 35 | # for a sane value of 100 commits) 36 | depth: $DEPTH_DEFAULT 37 | remotes: 38 | openg2p: https://github.com/OpenSPP/openg2p-program.git 39 | target: openg2p $ODOO_VERSION 40 | merges: 41 | - openg2p 17.0-develop-openspp 42 | # Example of a merge of the PR with the number 43 | # - oca refs/pull//head 44 | 45 | openg2p_rest_framework: 46 | defaults: 47 | # Shallow repositories ($DEPTH_DEFAULT=1) are faster & thinner 48 | # You may need a bigger depth when merging PRs (use $DEPTH_MERGE 49 | # for a sane value of 100 commits) 50 | depth: 1 51 | remotes: 52 | openg2p: https://github.com/OpenSPP/openg2p-rest-framework.git 53 | target: openg2p 17.0-openspp 54 | merges: 55 | - openg2p 17.0-openspp 56 | # Example of a merge of the PR with the number 57 | # - oca refs/pull//head 58 | 59 | openg2p_auth: 60 | defaults: 61 | # Shallow repositories ($DEPTH_DEFAULT=1) are faster & thinner 62 | # You may need a bigger depth when merging PRs (use $DEPTH_MERGE 63 | # for a sane value of 100 commits) 64 | depth: 1 65 | remotes: 66 | openg2p: https://github.com/OpenSPP/openg2p-auth.git 67 | target: openg2p 17.0-develop-openspp 68 | merges: 69 | - openg2p 17.0-develop-openspp 70 | # Example of a merge of the PR with the number 71 | # - oca refs/pull//head 72 | 73 | openspp_modules: 74 | defaults: 75 | # Shallow repositories ($DEPTH_DEFAULT=1) are faster & thinner 76 | # You may need a bigger depth when merging PRs (use $DEPTH_MERGE 77 | # for a sane value of 100 commits) 78 | depth: $DEPTH_DEFAULT 79 | remotes: 80 | openspp: https://github.com/openspp/openspp-modules.git 81 | target: openspp 17.0 82 | merges: 83 | - openspp 17.0 84 | 85 | muk_addons: 86 | remotes: 87 | muk: https://github.com/OpenSPP/mukit-modules.git 88 | target: muk $ODOO_VERSION 89 | merges: 90 | - muk 17.0-openspp 91 | 92 | server-ux: 93 | remotes: 94 | OCA: https://github.com/OCA/server-ux.git 95 | target: OCA 17.0 96 | merges: 97 | - OCA 17.0 98 | 99 | server-tools: 100 | remotes: 101 | OCA: https://github.com/OCA/server-tools.git 102 | target: OCA 17.0 103 | merges: 104 | - OCA 17.0 105 | 106 | queue: 107 | remotes: 108 | OCA: https://github.com/OCA/queue.git 109 | target: OCA 17.0 110 | merges: 111 | - OCA 17.0 112 | 113 | server-backend: 114 | remotes: 115 | OCA: https://github.com/OCA/server-backend.git 116 | target: OCA 17.0 117 | merges: 118 | - OCA 17.0 119 | 120 | web-api: 121 | remotes: 122 | OCA: https://github.com/OCA/web-api.git 123 | target: OCA 17.0 124 | merges: 125 | - OCA 17.0 126 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: | 2 | (?x) 3 | # NOT INSTALLABLE ADDONS 4 | # END NOT INSTALLABLE ADDONS 5 | # Files and folders generated by bots, to avoid loops 6 | ^setup/|/static/description/index\.html$| 7 | # We don't want to mess with tool-generated files 8 | .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| 9 | # Maybe reactivate this when all README files include prettier ignore tags? 10 | ^README\.md$| 11 | # Library files can have extraneous formatting (even minimized) 12 | /static/(src/)?lib/| 13 | # Repos using Sphinx to generate docs don't need prettying 14 | ^docs/_templates/.*\.html$| 15 | # Don't bother non-technical authors with formatting issues in docs 16 | readme/.*\.(rst|md)$| 17 | # Ignore build and dist directories in addons 18 | /build/|/dist/| 19 | # You don't usually want a bot to modify your legal texts 20 | (LICENSE.*|COPYING.*) 21 | default_language_version: 22 | python: python3 23 | node: "16.17.0" 24 | repos: 25 | - repo: local 26 | hooks: 27 | # These files are most likely copier diff rejection junks; if found, 28 | # review them manually, fix the problem (if needed) and remove them 29 | - id: forbidden-files 30 | name: forbidden files 31 | entry: found forbidden files; remove them 32 | language: fail 33 | files: "\\.rej$" 34 | - id: en-po-files 35 | name: en.po files cannot exist 36 | entry: found a en.po file 37 | language: fail 38 | files: '[a-zA-Z0-9_]*/i18n/en\.po$' 39 | - repo: https://github.com/sbidoul/whool 40 | rev: v0.5 41 | hooks: 42 | - id: whool-init 43 | - repo: https://github.com/oca/maintainer-tools 44 | rev: f71041f22b8cd68cf7c77b73a14ca8d8cd190a60 45 | hooks: 46 | # update the NOT INSTALLABLE ADDONS section above 47 | - id: oca-update-pre-commit-excluded-addons 48 | - id: oca-fix-manifest-website 49 | args: ["https://github.com/OCA/geospatial"] 50 | - id: oca-gen-addon-readme 51 | args: 52 | - --addons-dir=. 53 | - --branch=17.0 54 | - --org-name=OCA 55 | - --repo-name=geospatial 56 | - --if-source-changed 57 | - --keep-source-digest 58 | - --convert-fragments-to-markdown 59 | - id: oca-gen-external-dependencies 60 | - repo: https://github.com/OCA/odoo-pre-commit-hooks 61 | rev: v0.0.25 62 | hooks: 63 | - id: oca-checks-odoo-module 64 | - id: oca-checks-po 65 | - repo: https://github.com/pre-commit/mirrors-prettier 66 | rev: v2.7.1 67 | hooks: 68 | - id: prettier 69 | name: prettier (with plugin-xml) 70 | additional_dependencies: 71 | - "prettier@2.7.1" 72 | - "@prettier/plugin-xml@2.2.0" 73 | args: 74 | - --plugin=@prettier/plugin-xml 75 | files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$ 76 | - repo: https://github.com/pre-commit/mirrors-eslint 77 | rev: v8.24.0 78 | hooks: 79 | - id: eslint 80 | verbose: true 81 | args: 82 | - --color 83 | - --fix 84 | - repo: https://github.com/pre-commit/pre-commit-hooks 85 | rev: v4.3.0 86 | hooks: 87 | - id: trailing-whitespace 88 | # exclude autogenerated files 89 | exclude: /README\.rst$|\.pot?$ 90 | - id: end-of-file-fixer 91 | # exclude autogenerated files 92 | exclude: /README\.rst$|\.pot?$ 93 | - id: debug-statements 94 | - id: fix-encoding-pragma 95 | args: ["--remove"] 96 | - id: check-case-conflict 97 | - id: check-docstring-first 98 | - id: check-executables-have-shebangs 99 | - id: check-merge-conflict 100 | # exclude files where underlines are not distinguishable from merge conflicts 101 | exclude: /README\.rst$|^docs/.*\.rst$ 102 | - id: check-symlinks 103 | - id: check-xml 104 | - id: mixed-line-ending 105 | args: ["--fix=lf"] 106 | - repo: https://github.com/astral-sh/ruff-pre-commit 107 | rev: v0.1.3 108 | hooks: 109 | - id: ruff 110 | args: [--fix, --exit-non-zero-on-fix] 111 | - id: ruff-format 112 | - repo: https://github.com/OCA/pylint-odoo 113 | rev: v8.0.19 114 | hooks: 115 | - id: pylint_odoo 116 | name: pylint with optional checks 117 | args: 118 | - --rcfile=.pylintrc 119 | - --exit-zero 120 | verbose: true 121 | - id: pylint_odoo 122 | args: 123 | - --rcfile=.pylintrc-mandatory 124 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | 4 | # See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 5 | parserOptions: 6 | ecmaVersion: 2017 7 | 8 | # Globals available in Odoo that shouldn't produce errorings 9 | globals: 10 | _: readonly 11 | $: readonly 12 | fuzzy: readonly 13 | jQuery: readonly 14 | moment: readonly 15 | odoo: readonly 16 | openerp: readonly 17 | Promise: readonly 18 | 19 | # Styling is handled by Prettier, so we only need to enable AST rules; 20 | # see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890 21 | rules: 22 | accessor-pairs: warn 23 | array-callback-return: warn 24 | callback-return: warn 25 | capitalized-comments: 26 | - warn 27 | - always 28 | - ignoreConsecutiveComments: true 29 | ignoreInlineComments: true 30 | complexity: 31 | - warn 32 | - 15 33 | constructor-super: warn 34 | dot-notation: warn 35 | eqeqeq: warn 36 | global-require: warn 37 | handle-callback-err: warn 38 | id-blacklist: warn 39 | id-match: warn 40 | init-declarations: error 41 | max-depth: warn 42 | max-nested-callbacks: warn 43 | max-statements-per-line: warn 44 | no-alert: warn 45 | no-array-constructor: warn 46 | no-caller: warn 47 | no-case-declarations: warn 48 | no-class-assign: warn 49 | no-cond-assign: error 50 | no-const-assign: error 51 | no-constant-condition: warn 52 | no-control-regex: warn 53 | no-debugger: error 54 | no-delete-var: warn 55 | no-div-regex: warn 56 | no-dupe-args: error 57 | no-dupe-class-members: error 58 | no-dupe-keys: error 59 | no-duplicate-case: error 60 | no-duplicate-imports: error 61 | no-else-return: warn 62 | no-empty-character-class: warn 63 | no-empty-function: error 64 | no-empty-pattern: error 65 | no-empty: warn 66 | no-eq-null: error 67 | no-eval: error 68 | no-ex-assign: error 69 | no-extend-native: warn 70 | no-extra-bind: warn 71 | no-extra-boolean-cast: warn 72 | no-extra-label: warn 73 | no-fallthrough: warn 74 | no-func-assign: error 75 | no-global-assign: error 76 | no-implicit-coercion: 77 | - warn 78 | - allow: ["~"] 79 | no-implicit-globals: warn 80 | no-implied-eval: warn 81 | no-inline-comments: warn 82 | no-inner-declarations: warn 83 | no-invalid-regexp: warn 84 | no-irregular-whitespace: warn 85 | no-iterator: warn 86 | no-label-var: warn 87 | no-labels: warn 88 | no-lone-blocks: warn 89 | no-lonely-if: error 90 | no-mixed-requires: error 91 | no-multi-str: warn 92 | no-native-reassign: error 93 | no-negated-condition: warn 94 | no-negated-in-lhs: error 95 | no-new-func: warn 96 | no-new-object: warn 97 | no-new-require: warn 98 | no-new-symbol: warn 99 | no-new-wrappers: warn 100 | no-new: warn 101 | no-obj-calls: warn 102 | no-octal-escape: warn 103 | no-octal: warn 104 | no-param-reassign: warn 105 | no-path-concat: warn 106 | no-process-env: warn 107 | no-process-exit: warn 108 | no-proto: warn 109 | no-prototype-builtins: warn 110 | no-redeclare: warn 111 | no-regex-spaces: warn 112 | no-restricted-globals: warn 113 | no-restricted-imports: warn 114 | no-restricted-modules: warn 115 | no-restricted-syntax: warn 116 | no-return-assign: error 117 | no-script-url: warn 118 | no-self-assign: warn 119 | no-self-compare: warn 120 | no-sequences: warn 121 | no-shadow-restricted-names: warn 122 | no-shadow: warn 123 | no-sparse-arrays: warn 124 | no-sync: warn 125 | no-this-before-super: warn 126 | no-throw-literal: warn 127 | no-undef-init: warn 128 | no-undef: error 129 | no-unmodified-loop-condition: warn 130 | no-unneeded-ternary: error 131 | no-unreachable: error 132 | no-unsafe-finally: error 133 | no-unused-expressions: error 134 | no-unused-labels: error 135 | no-unused-vars: error 136 | no-use-before-define: error 137 | no-useless-call: warn 138 | no-useless-computed-key: warn 139 | no-useless-concat: warn 140 | no-useless-constructor: warn 141 | no-useless-escape: warn 142 | no-useless-rename: warn 143 | no-void: warn 144 | no-with: warn 145 | operator-assignment: [error, always] 146 | prefer-const: warn 147 | radix: warn 148 | require-yield: warn 149 | sort-imports: warn 150 | spaced-comment: [error, always] 151 | strict: [error, function] 152 | use-isnan: error 153 | valid-jsdoc: 154 | - warn 155 | - prefer: 156 | arg: param 157 | argument: param 158 | augments: extends 159 | constructor: class 160 | exception: throws 161 | func: function 162 | method: function 163 | prop: property 164 | return: returns 165 | virtual: abstract 166 | yield: yields 167 | preferType: 168 | array: Array 169 | bool: Boolean 170 | boolean: Boolean 171 | number: Number 172 | object: Object 173 | str: String 174 | string: String 175 | requireParamDescription: false 176 | requireReturn: false 177 | requireReturnDescription: false 178 | requireReturnType: false 179 | valid-typeof: warn 180 | yoda: warn 181 | -------------------------------------------------------------------------------- /devel.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | odoo_proxy: 3 | platform: linux/amd64 4 | image: ghcr.io/tecnativa/docker-whitelist:latest 5 | depends_on: 6 | - odoo 7 | networks: &public 8 | default: 9 | public: 10 | ports: 11 | - "127.0.0.1:17899:6899" 12 | - "127.0.0.1:17069:8069" 13 | - "127.0.0.1:17072:8072" 14 | environment: 15 | PORT: "6899 8069 8072" 16 | TARGET: odoo 17 | 18 | odoo: 19 | privileged: true 20 | extends: 21 | file: common.yaml 22 | service: odoo 23 | build: 24 | args: 25 | # To aggregate in development, use `setup-devel.yaml` 26 | AGGREGATE: "false" 27 | # Export these variables to own files created by odoo in your filesystem 28 | UID: "${UID:-1000}" 29 | GID: "${GID:-1000}" 30 | # No need for this in development 31 | PIP_INSTALL_ODOO: "false" 32 | CLEAN: "false" 33 | COMPILE: "false" 34 | environment: 35 | DOODBA_ENVIRONMENT: "${DOODBA_ENVIRONMENT-devel}" 36 | LIST_DB: "true" 37 | DEBUGPY_ENABLE: "${DOODBA_DEBUGPY_ENABLE:-0}" 38 | PGDATABASE: &dbname devel 39 | PYTHONDONTWRITEBYTECODE: 1 40 | PYTHONOPTIMIZE: "" 41 | PYTHONPATH: /opt/odoo/custom/src/odoo 42 | SMTP_PORT: "1025" 43 | WDB_WEB_PORT: "17984" 44 | # To avoid installing demo data export DOODBA_WITHOUT_DEMO=all 45 | WITHOUT_DEMO: "${DOODBA_WITHOUT_DEMO-false}" 46 | volumes: 47 | - ./odoo/custom:/opt/odoo/custom:rw,z 48 | - ./odoo/auto:/opt/odoo/auto:rw,z 49 | depends_on: 50 | - db 51 | - proxy_cdnjs_cloudflare_com 52 | - proxy_fonts_googleapis_com 53 | - proxy_fonts_gstatic_com 54 | - proxy_www_google_com 55 | - proxy_www_googleapis_com 56 | - proxy_www_gravatar_com 57 | - smtp 58 | - wdb 59 | command: 60 | - odoo 61 | - --limit-memory-soft=0 62 | - --limit-time-real-cron=9999999 63 | - --limit-time-real=9999999 64 | - --workers=0 65 | - --dev=reload,qweb,werkzeug,xml 66 | 67 | db: 68 | extends: 69 | file: common.yaml 70 | service: db 71 | networks: *public 72 | ports: 73 | - "127.0.0.1:17432:5432" 74 | environment: 75 | POSTGRES_DB: *dbname 76 | POSTGRES_PASSWORD: odoopassword 77 | 78 | pgweb: 79 | platform: linux/amd64 80 | image: docker.io/sosedoff/pgweb 81 | networks: *public 82 | ports: 83 | - "127.0.0.1:17081:8081" 84 | environment: 85 | DATABASE_URL: postgres://odoo:odoopassword@db:5432/devel?sslmode=disable 86 | depends_on: 87 | - db 88 | 89 | smtp: 90 | extends: 91 | file: common.yaml 92 | service: smtpfake 93 | networks: *public 94 | ports: 95 | - "127.0.0.1:17025:8025" 96 | 97 | wdb: 98 | platform: linux/amd64 99 | image: docker.io/kozea/wdb 100 | networks: *public 101 | ports: 102 | - "127.0.0.1:17984:1984" 103 | # HACK https://github.com/Kozea/wdb/issues/136 104 | init: true 105 | 106 | # Whitelist outgoing traffic for tests, reports, etc. 107 | proxy_cdnjs_cloudflare_com: 108 | platform: linux/amd64 109 | image: ghcr.io/tecnativa/docker-whitelist:latest 110 | networks: 111 | default: 112 | aliases: 113 | - cdnjs.cloudflare.com 114 | public: 115 | environment: 116 | TARGET: cdnjs.cloudflare.com 117 | PRE_RESOLVE: 1 118 | 119 | proxy_fonts_googleapis_com: 120 | platform: linux/amd64 121 | image: ghcr.io/tecnativa/docker-whitelist:latest 122 | networks: 123 | default: 124 | aliases: 125 | - fonts.googleapis.com 126 | public: 127 | environment: 128 | TARGET: fonts.googleapis.com 129 | PRE_RESOLVE: 1 130 | 131 | proxy_fonts_gstatic_com: 132 | platform: linux/amd64 133 | image: ghcr.io/tecnativa/docker-whitelist:latest 134 | networks: 135 | default: 136 | aliases: 137 | - fonts.gstatic.com 138 | public: 139 | environment: 140 | TARGET: fonts.gstatic.com 141 | PRE_RESOLVE: 1 142 | 143 | proxy_www_google_com: 144 | platform: linux/amd64 145 | image: ghcr.io/tecnativa/docker-whitelist:latest 146 | networks: 147 | default: 148 | aliases: 149 | - www.google.com 150 | public: 151 | environment: 152 | TARGET: www.google.com 153 | PRE_RESOLVE: 1 154 | 155 | proxy_www_googleapis_com: 156 | platform: linux/amd64 157 | image: ghcr.io/tecnativa/docker-whitelist:latest 158 | networks: 159 | default: 160 | aliases: 161 | - www.googleapis.com 162 | public: 163 | environment: 164 | TARGET: www.googleapis.com 165 | PRE_RESOLVE: 1 166 | 167 | proxy_www_gravatar_com: 168 | platform: linux/amd64 169 | image: ghcr.io/tecnativa/docker-whitelist:latest 170 | networks: 171 | default: 172 | aliases: 173 | - www.gravatar.com 174 | public: 175 | environment: 176 | TARGET: www.gravatar.com 177 | PRE_RESOLVE: 1 178 | 179 | networks: 180 | default: 181 | internal: ${DOODBA_NETWORK_INTERNAL-true} 182 | public: 183 | 184 | volumes: 185 | filestore: 186 | db: 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Doodba deployment](https://img.shields.io/badge/deployment-doodba-informational)](https://github.com/Tecnativa/doodba) 2 | [![Last template update](https://img.shields.io/badge/last%20template%20update-v6.0.1-informational)](https://github.com/Tecnativa/doodba-copier-template/tree/v6.0.1) 3 | [![Odoo](https://img.shields.io/badge/odoo-v17.0-a3478a)](https://github.com/odoo/odoo/tree/17.0) 4 | [![Apache-2.0 license](https://img.shields.io/badge/license-Apache--2.0-success})](LICENSE) 5 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://pre-commit.com/) 6 | 7 | # OpenSPP Docker Deployment 8 | 9 | This repository contains a [Doodba](https://github.com/Tecnativa/doodba)-based Docker deployment for [OpenSPP](https://openspp.org) - the Open Source Social Protection Platform. 10 | 11 | ## What is OpenSPP? 12 | 13 | OpenSPP is an open-source, modular, and highly interoperable digital platform designed to support social protection programs. It helps governments and humanitarian organizations digitalize their social protection systems efficiently and cost-effectively. 14 | 15 | The platform offers four main products: 16 | - **SP-MIS**: A comprehensive Social Protection Management Information System 17 | - **Social Registry**: For storing and managing data for social protection planning and administration 18 | - **Farmer Registry**: For managing farm holding and farm owner data 19 | - **Disability Registry**: For recording and managing information about individuals with disabilities 20 | 21 | OpenSPP is a Digital Public Good built on more than 60 open-source modules and leverages other open-source projects including OpenG2P, MOSIP, OpenCRVS, Odoo, Payment Hub EE, and more. 22 | 23 | ## Quick Start 24 | 25 | The official quick start guide is located [here](https://docs.openspp.org/getting_started/installation_guide.html). 26 | 27 | ### Requirements 28 | 29 | - Docker and Docker Compose 30 | - Git 31 | - Python 3.8+ with pip 32 | - Invoke (`pip install invoke`) 33 | - Git-aggregator (`pip install git-aggregator`) 34 | 35 | ### Setup 36 | 37 | 1. Clone this repository: 38 | ```bash 39 | git clone https://github.com/OpenSPP/openspp-docker.git 40 | cd openspp-docker 41 | ``` 42 | 43 | 2. Set up the development environment: 44 | ```bash 45 | invoke develop 46 | ``` 47 | 48 | 3. Pull and build images: 49 | ```bash 50 | invoke img-pull 51 | invoke img-build 52 | ``` 53 | 54 | 4. Download code repositories: 55 | ```bash 56 | invoke git-aggregate 57 | ``` 58 | 59 | Note: If you encounter SSH issues with git-aggregate, you can try: 60 | ```bash 61 | invoke git-aggregate-host 62 | ``` 63 | 64 | 5. Create a fresh database: 65 | ```bash 66 | invoke resetdb 67 | ``` 68 | 69 | 6. Start the environment: 70 | ```bash 71 | invoke start 72 | ``` 73 | 74 | 7. Access OpenSPP at http://localhost:17069 75 | 76 | ### Configuration 77 | 78 | Adjust the openspp-modules branch in `odoo/custom/src/repos.yaml` to the desired release. 79 | Currently, the OpenSPP Batanes release (17.0.1.2.1) is used. 80 | 81 | ## Common Operations 82 | 83 | - Stop the environment: `invoke stop` 84 | - Restart Odoo: `invoke restart` 85 | - View logs: `invoke logs` 86 | - Install modules: `invoke install --modules=module1,module2` 87 | - Update modules: `invoke update` 88 | - Run tests: `invoke test --modules=module1,module2` 89 | - Create a database snapshot: `invoke snapshot` 90 | - Restore a snapshot: `invoke restore-snapshot` 91 | 92 | ## Translations 93 | 94 | ### Managing Translations in OpenSPP 95 | 96 | To extract translatable strings and generate/update translation files (.pot and .po): 97 | 98 | 1. **First, install the modules** you want to translate 99 | 2. **Then extract translation files** using the update_pot task: 100 | ```bash 101 | invoke update-pot --modules="module1,module2,module3" --database=devel --msgmerge 102 | ``` 103 | 104 | ### Translation Options 105 | 106 | The `update-pot` task supports several options: 107 | 108 | - `--addons-dir`: Directory containing the addons (default: odoo/custom/src) 109 | - `--modules`: Comma-separated list of modules to process 110 | - `--database`: Database name to use (default: devel) 111 | - `--no-fuzzy`: Disable fuzzy matching when merging translations 112 | - `--update-po`: Update .po files after generating .pot files 113 | - `--lang`: Language code for PO files to update/create 114 | - `--force`: Force update of existing PO files 115 | - `--msgmerge`: Run msgmerge if POT file is created/updated 116 | - `--create-i18n`: Create i18n directories if they don't exist (default: True) 117 | - `--debug`: Enable more verbose logging 118 | 119 | ### Common Translation Workflows 120 | 121 | 1. **Generate POT files for specific modules**: 122 | ```bash 123 | invoke update-pot --modules="spp_base,spp_programs,spp_registry_base" --msgmerge 124 | ``` 125 | 126 | 2. **Update existing PO files for all languages**: 127 | ```bash 128 | invoke update-pot --modules="spp_base" --update-po --msgmerge 129 | ``` 130 | 131 | 3. **Create or update a specific language translation**: 132 | ```bash 133 | invoke update-pot --modules="spp_programs" --update-po --lang=fr_FR 134 | ``` 135 | 136 | **Note**: Modules must be installed in the database before translations can be extracted, as the extraction process uses Odoo's translation export mechanism which requires access to installed modules. 137 | 138 | ### Translation Best Practices 139 | 140 | When writing code that includes translatable strings, follow these best practices to avoid extraction errors: 141 | 142 | 1. **Never use f-strings within translation calls**: 143 | ```python 144 | # WRONG - will cause extraction errors 145 | _(f"Hello {user.name}") 146 | 147 | # CORRECT - use positional arguments with %s 148 | _("Hello %s") % user.name 149 | ``` 150 | 151 | 2. **Prefer positional arguments over named placeholders**: 152 | ```python 153 | # Less preferred 154 | _("Hello %(name)s") % {"name": user.name} 155 | 156 | # Preferred 157 | _("Hello %s") % user.name 158 | ``` 159 | 160 | 3. **Keep the translatable string and its variables separate**: 161 | ```python 162 | # WRONG - string formatting happens before translation 163 | _("Hello " + user.name) 164 | 165 | # CORRECT - translate the template, then insert variables 166 | _("Hello %s") % user.name 167 | ``` 168 | 169 | 4. **Handle plurals correctly**: 170 | ```python 171 | # Use Odoo's ngettext for pluralization 172 | ngettext( 173 | "You have %d message", 174 | "You have %d messages", 175 | count 176 | ) % count 177 | ``` 178 | 179 | Following these practices ensures that translation extraction works correctly and translators can work with complete sentences. 180 | 181 | ## Services 182 | 183 | The deployment includes the following services: 184 | 185 | - **Odoo**: The application server running OpenSPP modules 186 | - **PostgreSQL**: Database server with PostGIS extensions 187 | - **Mailhog**: Fake SMTP server for email testing (http://localhost:17025) 188 | - **pgweb**: Web-based PostgreSQL browser (http://localhost:17081) 189 | - **debugger**: Web-based debugger interface (http://localhost:17984) 190 | 191 | ## Documentation 192 | 193 | For more information about OpenSPP, please refer to: 194 | - [OpenSPP Documentation](https://docs.openspp.org/) 195 | - [OpenSPP GitHub](https://github.com/OpenSPP) 196 | 197 | For more information about Doodba, check these resources: 198 | - [General Doodba docs](https://github.com/Tecnativa/doodba) 199 | - [Doodba copier template docs](https://github.com/Tecnativa/doodba-copier-template) 200 | - [Doodba QA docs](https://github.com/Tecnativa/doodba-qa) 201 | 202 | ## Contributing 203 | 204 | Contributions to OpenSPP are welcome! Please refer to the [OpenSPP Contributor Guidelines](https://github.com/OpenSPP/openspp-modules/blob/17.0/CONTRIBUTING.md) for more information. 205 | 206 | ## Credits 207 | 208 | This project is maintained by the OpenSPP community. 209 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | """Doodba child project tasks. 2 | 3 | This file is to be executed with https://www.pyinvoke.org/ in Python 3.8.1+. 4 | 5 | Contains common helpers to develop using this child project. 6 | """ 7 | import json 8 | import os 9 | import shutil 10 | import stat 11 | import tempfile 12 | import time 13 | from datetime import datetime 14 | from itertools import chain 15 | from logging import getLogger 16 | from pathlib import Path 17 | from shutil import which 18 | 19 | from invoke import exceptions, task 20 | 21 | try: 22 | import yaml 23 | except ImportError: 24 | from invoke.util import yaml 25 | 26 | PROJECT_ROOT = Path(__file__).parent.absolute() 27 | SRC_PATH = PROJECT_ROOT / "odoo" / "custom" / "src" 28 | UID_ENV = { 29 | "GID": os.environ.get("DOODBA_GID", str(os.getgid())), 30 | "UID": os.environ.get("DOODBA_UID", str(os.getuid())), 31 | "DOODBA_UMASK": os.environ.get("DOODBA_UMASK", "27"), 32 | } 33 | UID_ENV.update( 34 | { 35 | "DOODBA_GITAGGREGATE_GID": os.environ.get( 36 | "DOODBA_GITAGGREGATE_GID", UID_ENV["GID"] 37 | ), 38 | "DOODBA_GITAGGREGATE_UID": os.environ.get( 39 | "DOODBA_GITAGGREGATE_UID", UID_ENV["UID"] 40 | ), 41 | } 42 | ) 43 | SERVICES_WAIT_TIME = int(os.environ.get("SERVICES_WAIT_TIME", 4)) 44 | ODOO_VERSION = float( 45 | yaml.safe_load((PROJECT_ROOT / "common.yaml").read_text())["services"]["odoo"][ 46 | "build" 47 | ]["args"]["ODOO_VERSION"] 48 | ) 49 | DOCKER_COMPOSE_CMD = f"{shutil.which('docker')} compose" or shutil.which( 50 | "docker-compose" 51 | ) 52 | 53 | _logger = getLogger(__name__) 54 | 55 | 56 | def _override_docker_command(service, command, file, orig_file=None): 57 | # Read config from main file 58 | default_compose_file_version = "2.4" 59 | if orig_file: 60 | with open(orig_file) as fd: 61 | orig_docker_config = yaml.safe_load(fd.read()) 62 | docker_compose_file_version = orig_docker_config.get( 63 | "version", default_compose_file_version 64 | ) 65 | else: 66 | docker_compose_file_version = default_compose_file_version 67 | docker_config = { 68 | "version": docker_compose_file_version, 69 | "services": {service: {"command": command}}, 70 | } 71 | docker_config_yaml = yaml.dump(docker_config) 72 | file.write(docker_config_yaml) 73 | file.flush() 74 | 75 | 76 | def _remove_auto_reload(file, orig_file): 77 | with open(orig_file) as fd: 78 | orig_docker_config = yaml.safe_load(fd.read()) 79 | odoo_command = orig_docker_config["services"]["odoo"]["command"] 80 | new_odoo_command = [] 81 | for flag in odoo_command: 82 | if flag.startswith("--dev"): 83 | flag = flag.replace("reload,", "") 84 | new_odoo_command.append(flag) 85 | _override_docker_command("odoo", new_odoo_command, file, orig_file=orig_file) 86 | 87 | 88 | def _get_cwd_addon(file): 89 | cwd = Path(file).resolve() 90 | manifest_file = False 91 | while PROJECT_ROOT < cwd: 92 | manifest_file = (cwd / "__manifest__.py").exists() or ( 93 | cwd / "__openerp__.py" 94 | ).exists() 95 | if manifest_file: 96 | return cwd.stem 97 | cwd = cwd.parent 98 | if cwd == PROJECT_ROOT: 99 | return None 100 | 101 | 102 | @task 103 | def write_code_workspace_file(c, cw_path=None): 104 | """Generate code-workspace file definition. 105 | 106 | Some other tasks will call this one when needed, and since you cannot specify 107 | the file name there, if you want a specific one, you should call this task 108 | before. 109 | 110 | Most times you just can forget about this task and let it be run automatically 111 | whenever needed. 112 | 113 | If you don't define a workspace name, this task will reuse the 1st 114 | `doodba.*.code-workspace` file found inside the current directory. 115 | If none is found, it will default to `doodba.$(basename $PWD).code-workspace`. 116 | 117 | If you define it manually, remember to use the same prefix and suffix if you 118 | want it git-ignored by default. 119 | Example: `--cw-path doodba.my-custom-name.code-workspace` 120 | """ 121 | root_name = f"doodba.{PROJECT_ROOT.name}" 122 | root_var = "${workspaceFolder:%s}" % root_name 123 | if not cw_path: 124 | try: 125 | cw_path = next(PROJECT_ROOT.glob("doodba.*.code-workspace")) 126 | except StopIteration: 127 | cw_path = f"{root_name}.code-workspace" 128 | if not Path(cw_path).is_absolute(): 129 | cw_path = PROJECT_ROOT / cw_path 130 | cw_config = {} 131 | try: 132 | with open(cw_path) as cw_fd: 133 | cw_config = json.load(cw_fd) 134 | except (FileNotFoundError, json.decoder.JSONDecodeError): 135 | pass # Nevermind, we start with a new config 136 | # Static settings 137 | cw_config.setdefault("settings", {}) 138 | cw_config["settings"].update( 139 | { 140 | "python.autoComplete.extraPaths": [f"{str(SRC_PATH)}/odoo"], 141 | "python.formatting.provider": "none", 142 | "python.linting.flake8Enabled": True, 143 | "python.linting.ignorePatterns": [f"{str(SRC_PATH)}/odoo/**/*.py"], 144 | "python.linting.pylintArgs": [ 145 | f"--init-hook=\"import sys;sys.path.append('{str(SRC_PATH)}/odoo')\"", 146 | "--load-plugins=pylint_odoo", 147 | ], 148 | "python.linting.pylintEnabled": True, 149 | "python.defaultInterpreterPath": "python%s" 150 | % (2 if ODOO_VERSION < 11 else 3), 151 | "restructuredtext.confPath": "", 152 | "search.followSymlinks": False, 153 | "search.useIgnoreFiles": False, 154 | # Language-specific configurations 155 | "[python]": {"editor.defaultFormatter": "ms-python.black-formatter"}, 156 | "[json]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}, 157 | "[jsonc]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}, 158 | "[markdown]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}, 159 | "[yaml]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}, 160 | "[xml]": {"editor.formatOnSave": False}, 161 | } 162 | ) 163 | # Launch configurations 164 | debugpy_configuration = { 165 | "name": "Attach Python debugger to running container", 166 | "type": "python", 167 | "request": "attach", 168 | "pathMappings": [], 169 | "port": int(ODOO_VERSION) * 1000 + 899, 170 | # HACK https://github.com/microsoft/vscode-python/issues/14820 171 | "host": "0.0.0.0", 172 | } 173 | firefox_configuration = { 174 | "type": "firefox", 175 | "request": "launch", 176 | "reAttach": True, 177 | "name": "Connect to firefox debugger", 178 | "url": f"http://localhost:{ODOO_VERSION:.0f}069/?debug=assets", 179 | "reloadOnChange": { 180 | "watch": f"{root_var}/odoo/custom/src/**/*.{'{js,css,scss,less}'}" 181 | }, 182 | "skipFiles": ["**/lib/**"], 183 | "pathMappings": [], 184 | } 185 | chrome_executable = which("chrome") or which("chromium") 186 | chrome_configuration = { 187 | "type": "chrome", 188 | "request": "launch", 189 | "name": "Connect to chrome debugger", 190 | "url": f"http://localhost:{ODOO_VERSION:.0f}069/?debug=assets", 191 | "skipFiles": ["**/lib/**"], 192 | "trace": True, 193 | "pathMapping": {}, 194 | } 195 | if chrome_executable: 196 | chrome_configuration["runtimeExecutable"] = chrome_executable 197 | cw_config["launch"] = { 198 | "compounds": [ 199 | { 200 | "name": "Start Odoo and debug Python", 201 | "configurations": ["Attach Python debugger to running container"], 202 | "preLaunchTask": "Start Odoo in debug mode", 203 | }, 204 | { 205 | "name": "Test and debug current module", 206 | "configurations": ["Attach Python debugger to running container"], 207 | "preLaunchTask": "Run Odoo Tests in debug mode for current module", 208 | "internalConsoleOptions": "openOnSessionStart", 209 | }, 210 | ], 211 | "configurations": [ 212 | debugpy_configuration, 213 | firefox_configuration, 214 | chrome_configuration, 215 | ], 216 | } 217 | # Configure folders and debuggers 218 | debugpy_configuration["pathMappings"].append( 219 | { 220 | "localRoot": "${workspaceFolder:odoo}/", 221 | "remoteRoot": "/opt/odoo/custom/src/odoo", 222 | } 223 | ) 224 | cw_config["folders"] = [] 225 | for subrepo in SRC_PATH.glob("*"): 226 | if not subrepo.is_dir(): 227 | continue 228 | if (subrepo / ".git").exists() and subrepo.name != "odoo": 229 | cw_config["folders"].append( 230 | {"path": str(subrepo.relative_to(PROJECT_ROOT))} 231 | ) 232 | for addon in chain(subrepo.glob("*"), subrepo.glob("addons/*")): 233 | if (addon / "__manifest__.py").is_file() or ( 234 | addon / "__openerp__.py" 235 | ).is_file(): 236 | if subrepo.name == "odoo": 237 | # ruff: noqa: UP031 238 | local_path = "${workspaceFolder:%s}/addons/%s/" % ( 239 | subrepo.name, 240 | addon.name, 241 | ) 242 | else: 243 | local_path = "${workspaceFolder:%s}/%s" % (subrepo.name, addon.name) 244 | debugpy_configuration["pathMappings"].append( 245 | { 246 | "localRoot": local_path, 247 | "remoteRoot": f"/opt/odoo/auto/addons/{addon.name}/", 248 | } 249 | ) 250 | url = f"http://localhost:{ODOO_VERSION:.0f}069/{addon.name}/static/" 251 | path = "${workspaceFolder:%s}/%s/static/" % ( 252 | subrepo.name, 253 | addon.relative_to(subrepo), 254 | ) 255 | firefox_configuration["pathMappings"].append({"url": url, "path": path}) 256 | chrome_configuration["pathMapping"][url] = path 257 | cw_config["tasks"] = { 258 | "version": "2.0.0", 259 | "tasks": [ 260 | { 261 | "label": "Start Odoo", 262 | "type": "process", 263 | "command": "invoke", 264 | "args": ["start", "--detach"], 265 | "presentation": { 266 | "echo": True, 267 | "reveal": "silent", 268 | "focus": False, 269 | "panel": "shared", 270 | "showReuseMessage": True, 271 | "clear": False, 272 | }, 273 | "problemMatcher": [], 274 | "options": {"statusbar": {"label": "$(play-circle) Start Odoo"}}, 275 | }, 276 | { 277 | "label": "Install current module", 278 | "type": "process", 279 | "command": "invoke", 280 | "args": ["install", "--cur-file", "${file}", "restart"], 281 | "presentation": { 282 | "echo": True, 283 | "reveal": "always", 284 | "focus": True, 285 | "panel": "shared", 286 | "showReuseMessage": True, 287 | "clear": False, 288 | }, 289 | "problemMatcher": [], 290 | "options": { 291 | "statusbar": {"label": "$(symbol-property) Install module"} 292 | }, 293 | }, 294 | { 295 | "label": "Run Odoo Tests for current module", 296 | "type": "process", 297 | "command": "invoke", 298 | "args": ["test", "--cur-file", "${file}"], 299 | "presentation": { 300 | "echo": True, 301 | "reveal": "always", 302 | "focus": True, 303 | "panel": "shared", 304 | "showReuseMessage": True, 305 | "clear": False, 306 | }, 307 | "problemMatcher": [], 308 | "options": {"statusbar": {"label": "$(beaker) Test module"}}, 309 | }, 310 | { 311 | "label": "Run Odoo Tests in debug mode for current module", 312 | "type": "process", 313 | "command": "invoke", 314 | "args": [ 315 | "test", 316 | "--cur-file", 317 | "${file}", 318 | "--debugpy", 319 | ], 320 | "presentation": { 321 | "echo": True, 322 | "reveal": "silent", 323 | "focus": False, 324 | "panel": "shared", 325 | "showReuseMessage": True, 326 | "clear": False, 327 | }, 328 | "problemMatcher": [], 329 | "options": {"statusbar": {"hide": True}}, 330 | }, 331 | { 332 | "label": "Start Odoo in debug mode", 333 | "type": "process", 334 | "command": "invoke", 335 | "args": ["start", "--detach", "--debugpy"], 336 | "presentation": { 337 | "echo": True, 338 | "reveal": "silent", 339 | "focus": False, 340 | "panel": "shared", 341 | "showReuseMessage": True, 342 | "clear": False, 343 | }, 344 | "problemMatcher": [], 345 | "options": {"statusbar": {"hide": True}}, 346 | }, 347 | { 348 | "label": "Stop Odoo", 349 | "type": "process", 350 | "command": "invoke", 351 | "args": ["stop"], 352 | "presentation": { 353 | "echo": True, 354 | "reveal": "silent", 355 | "focus": False, 356 | "panel": "shared", 357 | "showReuseMessage": True, 358 | "clear": False, 359 | }, 360 | "problemMatcher": [], 361 | "options": {"statusbar": {"label": "$(stop-circle) Stop Odoo"}}, 362 | }, 363 | { 364 | "label": "Restart Odoo", 365 | "type": "process", 366 | "command": "invoke", 367 | "args": ["restart"], 368 | "presentation": { 369 | "echo": True, 370 | "reveal": "silent", 371 | "focus": False, 372 | "panel": "shared", 373 | "showReuseMessage": True, 374 | "clear": False, 375 | }, 376 | "problemMatcher": [], 377 | "options": {"statusbar": {"label": "$(history) Restart Odoo"}}, 378 | }, 379 | { 380 | "label": "See container logs", 381 | "type": "process", 382 | "command": "invoke", 383 | "args": ["logs"], 384 | "presentation": { 385 | "echo": True, 386 | "reveal": "always", 387 | "focus": False, 388 | "panel": "shared", 389 | "showReuseMessage": True, 390 | "clear": False, 391 | }, 392 | "problemMatcher": [], 393 | "options": { 394 | "statusbar": {"label": "$(list-selection) See container logs"} 395 | }, 396 | }, 397 | ], 398 | } 399 | # Sort project folders 400 | cw_config["folders"].sort(key=lambda x: x["path"]) 401 | # Put Odoo folder just before private and top folder and map to debugpy 402 | odoo = SRC_PATH / "odoo" 403 | if odoo.is_dir(): 404 | cw_config["folders"].append({"path": str(odoo.relative_to(PROJECT_ROOT))}) 405 | # HACK https://github.com/microsoft/vscode/issues/95963 put private second to last 406 | private = SRC_PATH / "private" 407 | if private.is_dir(): 408 | cw_config["folders"].append({"path": str(private.relative_to(PROJECT_ROOT))}) 409 | # HACK https://github.com/microsoft/vscode/issues/37947 put top folder last 410 | cw_config["folders"].append({"path": ".", "name": root_name}) 411 | with open(cw_path, "w") as cw_fd: 412 | json.dump(cw_config, cw_fd, indent=2) 413 | cw_fd.write("\n") 414 | 415 | 416 | @task 417 | def develop(c): 418 | """Set up a basic development environment.""" 419 | # Prepare environment 420 | auto = Path(PROJECT_ROOT, "odoo", "auto") 421 | addons = auto / "addons" 422 | addons.mkdir(parents=True, exist_ok=True) 423 | # Allow others writing, for podman support 424 | auto.chmod(0o777) 425 | addons.chmod(0o777) 426 | with c.cd(str(PROJECT_ROOT)): 427 | c.run("git init") 428 | c.run("ln -sf devel.yaml docker-compose.yml") 429 | write_code_workspace_file(c) 430 | c.run("pre-commit install") 431 | 432 | 433 | @task(develop) 434 | def git_aggregate(c): 435 | """Download odoo & addons git code. 436 | 437 | Executes git-aggregator from within the doodba container. 438 | """ 439 | with c.cd(str(PROJECT_ROOT)): 440 | c.run( 441 | DOCKER_COMPOSE_CMD + " --file setup-devel.yaml run --rm -T odoo", 442 | env=UID_ENV, 443 | ) 444 | write_code_workspace_file(c) 445 | for git_folder in SRC_PATH.glob("*/.git/.."): 446 | action = ( 447 | "install" 448 | if (git_folder / ".pre-commit-config.yaml").is_file() 449 | else "uninstall" 450 | ) 451 | with c.cd(str(git_folder)): 452 | c.run(f"pre-commit {action}") 453 | 454 | 455 | @task(develop) 456 | def git_aggregate_host(c): 457 | """Download odoo & addons git code directly on the host. 458 | 459 | Executes git-aggregator on the host machine, avoiding Docker SSH agent issues. 460 | """ 461 | # Define paths 462 | src_path = PROJECT_ROOT / "odoo" / "custom" / "src" 463 | repos_yaml = src_path / "repos.yaml" 464 | 465 | # Create src directory if it doesn't exist 466 | src_path.mkdir(parents=True, exist_ok=True) 467 | 468 | # Print the paths being used for clarity 469 | _logger.info("Project root: %s", PROJECT_ROOT) 470 | _logger.info("Source path: %s", src_path) 471 | _logger.info("Repos YAML: %s", repos_yaml) 472 | 473 | # Define environment variables used in repos.yaml 474 | env = { 475 | "DEPTH_DEFAULT": "1", # Default depth for shallow clones 476 | "DEPTH_MERGE": "100", # Depth when merging PRs 477 | "ODOO_VERSION": f"{ODOO_VERSION:.1f}", # Make available for repos.yaml substitution 478 | "PATH": os.environ.get("PATH", ""), 479 | } 480 | 481 | # Important: Change directory to src_path before running git-aggregator 482 | _logger.info( 483 | "\nRunning git-aggregator from the src directory to ensure correct repository placement..." 484 | ) 485 | with c.cd(str(src_path)): 486 | # Run git-aggregator 487 | _logger.info("Starting git-aggregator...") 488 | try: 489 | # Use the relative path to repos.yaml (just the filename when in the same directory) 490 | result = c.run( 491 | "gitaggregate -c repos.yaml --expand-env aggregate", 492 | env=env, 493 | pty=True, 494 | ) 495 | 496 | if result.ok: 497 | _logger.info("\nGit aggregation completed successfully!") 498 | _logger.info("Repositories have been cloned into: %s", src_path) 499 | else: 500 | _logger.info("\nGit aggregation failed. Check the errors above.") 501 | except Exception as e: 502 | _logger.info("\nError running git-aggregator: %s", e) 503 | _logger.info("\nTrying without environment variable expansion...") 504 | try: 505 | result = c.run("gitaggregate -c repos.yaml aggregate", pty=True) 506 | if result.ok: 507 | _logger.info("\nGit aggregation completed successfully!") 508 | _logger.info("Repositories have been cloned into: %s", src_path) 509 | except Exception as e2: 510 | _logger.info( 511 | "\nError running git-aggregator without env expansion: %s", e2 512 | ) 513 | 514 | write_code_workspace_file(c) 515 | for git_folder in SRC_PATH.glob("*/.git/.."): 516 | action = ( 517 | "install" 518 | if (git_folder / ".pre-commit-config.yaml").is_file() 519 | else "uninstall" 520 | ) 521 | with c.cd(str(git_folder)): 522 | c.run(f"pre-commit {action}") 523 | 524 | 525 | @task(develop) 526 | def closed_prs(c): 527 | """Test closed PRs from repos.yaml""" 528 | with c.cd(str(PROJECT_ROOT / "odoo/custom/src")): 529 | cmd = "gitaggregate -c {} show-closed-prs".format("repos.yaml") 530 | c.run(cmd, env=UID_ENV, pty=True) 531 | 532 | 533 | @task() 534 | def img_build(c, pull=True): 535 | """Build docker images.""" 536 | cmd = DOCKER_COMPOSE_CMD + " build" 537 | if pull: 538 | cmd += " --pull" 539 | with c.cd(str(PROJECT_ROOT)): 540 | c.run(cmd, env=UID_ENV, pty=True) 541 | 542 | 543 | @task() 544 | def img_pull(c): 545 | """Pull docker images.""" 546 | with c.cd(str(PROJECT_ROOT)): 547 | c.run(DOCKER_COMPOSE_CMD + " pull", pty=True) 548 | 549 | 550 | @task() 551 | def lint(c, verbose=False): 552 | """Lint & format source code.""" 553 | cmd = "pre-commit run --show-diff-on-failure --all-files --color=always" 554 | if verbose: 555 | cmd += " --verbose" 556 | with c.cd(str(PROJECT_ROOT)): 557 | c.run(cmd) 558 | 559 | 560 | @task() 561 | def start(c, detach=True, debugpy=False): 562 | """Start environment.""" 563 | cmd = DOCKER_COMPOSE_CMD + " up" 564 | with tempfile.NamedTemporaryFile( 565 | mode="w", 566 | suffix=".yaml", 567 | ) as tmp_docker_compose_file: 568 | if debugpy: 569 | # Remove auto-reload 570 | cmd = ( 571 | DOCKER_COMPOSE_CMD + " -f docker-compose.yml " 572 | f"-f {tmp_docker_compose_file.name} up" 573 | ) 574 | _remove_auto_reload( 575 | tmp_docker_compose_file, 576 | orig_file=PROJECT_ROOT / "docker-compose.yml", 577 | ) 578 | if detach: 579 | cmd += " --detach" 580 | with c.cd(str(PROJECT_ROOT)): 581 | result = c.run( 582 | cmd, 583 | pty=True, 584 | env=dict( 585 | UID_ENV, 586 | DOODBA_DEBUGPY_ENABLE=str(int(debugpy)), 587 | ), 588 | ) 589 | if not ( 590 | "Recreating" in result.stdout 591 | or "Starting" in result.stdout 592 | or "Creating" in result.stdout 593 | ): 594 | restart(c) 595 | _logger.info("Waiting for services to spin up...") 596 | time.sleep(SERVICES_WAIT_TIME) 597 | 598 | 599 | @task( 600 | help={ 601 | "modules": "Comma-separated list of modules to install.", 602 | "dbname": "Target database name (defaults to PGDATABASE/devel).", 603 | "core": "Install all core addons. Default: False", 604 | "extra": "Install all extra addons. Default: False", 605 | "private": "Install all private addons. Default: False", 606 | "enterprise": "Install all enterprise addons. Default: False", 607 | "cur-file": "Path to the current file. Addon name will be obtained from there to install.", 608 | }, 609 | ) 610 | def install( 611 | c, 612 | modules=None, 613 | cur_file=None, 614 | core=False, 615 | extra=False, 616 | private=False, 617 | enterprise=False, 618 | dbname=None, 619 | ): 620 | """Install Odoo addons 621 | 622 | By default, installs addon from directory being worked on, 623 | unless other options are specified. 624 | """ 625 | if not (modules or core or extra or private or enterprise): 626 | cur_module = _get_cwd_addon(cur_file or Path.cwd()) 627 | if not cur_module: 628 | raise exceptions.ParseError( 629 | msg="Odoo addon to install not found. " 630 | "You must provide at least one option for modules" 631 | " or be in a subdirectory of one." 632 | " See --help for details." 633 | ) 634 | modules = cur_module 635 | target_db = dbname or os.environ.get("PGDATABASE") or "devel" 636 | # Prefer direct Odoo CLI with explicit DB when modules are provided 637 | if modules and not (core or extra or private or enterprise): 638 | cmd = ( 639 | DOCKER_COMPOSE_CMD 640 | + f" run --rm -e DB_FILTER=^{target_db}$ odoo odoo --stop-after-init -d {target_db} -i {modules}" 641 | ) 642 | else: 643 | cmd = DOCKER_COMPOSE_CMD + " run --rm odoo addons init" 644 | if core: 645 | cmd += " --core" 646 | if extra: 647 | cmd += " --extra" 648 | if private: 649 | cmd += " --private" 650 | if enterprise: 651 | cmd += " --enterprise" 652 | if modules: 653 | cmd += f" -w {modules}" 654 | with c.cd(str(PROJECT_ROOT)): 655 | c.run(DOCKER_COMPOSE_CMD + " stop odoo") 656 | c.run( 657 | cmd, 658 | env=UID_ENV, 659 | pty=True, 660 | ) 661 | 662 | 663 | @task( 664 | help={ 665 | "modules": "Comma-separated list of modules to uninstall.", 666 | }, 667 | ) 668 | def uninstall( 669 | c, 670 | modules=None, 671 | cur_file=None, 672 | ): 673 | """Uninstall Odoo addons 674 | 675 | By default, uninstalls addon from directory being worked on, 676 | unless other options are specified. 677 | """ 678 | if not modules: 679 | cur_module = _get_cwd_addon(cur_file or Path.cwd()) 680 | if not cur_module: 681 | raise exceptions.ParseError( 682 | msg="Odoo addon to uninstall not found. " 683 | "You must provide at least one option for modules" 684 | " or be in a subdirectory of one." 685 | " See --help for details." 686 | ) 687 | modules = cur_module 688 | cmd = ( 689 | DOCKER_COMPOSE_CMD 690 | + f" run --rm odoo click-odoo-uninstall -m {modules or cur_module}" 691 | ) 692 | with c.cd(str(PROJECT_ROOT)): 693 | c.run( 694 | cmd, 695 | env=UID_ENV, 696 | pty=True, 697 | ) 698 | 699 | 700 | def _get_module_dependencies( 701 | c, modules=None, core=False, extra=False, private=False, enterprise=False 702 | ): 703 | """Returns a list of the addons' dependencies 704 | 705 | By default, refers to the addon from directory being worked on, 706 | unless other options are specified. 707 | """ 708 | # Get list of dependencies for addon 709 | cmd = DOCKER_COMPOSE_CMD + " run --rm odoo addons list --dependencies" 710 | if core: 711 | cmd += " --core" 712 | if extra: 713 | cmd += " --extra" 714 | if private: 715 | cmd += " --private" 716 | if enterprise: 717 | cmd += " --enterprise" 718 | if modules: 719 | cmd += f" -w {modules}" 720 | with c.cd(str(PROJECT_ROOT)): 721 | dependencies = c.run( 722 | cmd, 723 | env=UID_ENV, 724 | hide="stdout", 725 | ).stdout.splitlines()[-1] 726 | return dependencies 727 | 728 | 729 | def _test_in_debug_mode(c, odoo_command): 730 | with tempfile.NamedTemporaryFile( 731 | mode="w", suffix=".yaml" 732 | ) as tmp_docker_compose_file: 733 | cmd = ( 734 | DOCKER_COMPOSE_CMD + " -f docker-compose.yml " 735 | f"-f {tmp_docker_compose_file.name} up -d" 736 | ) 737 | _override_docker_command( 738 | "odoo", 739 | odoo_command, 740 | file=tmp_docker_compose_file, 741 | orig_file=Path(str(PROJECT_ROOT), "docker-compose.yml"), 742 | ) 743 | with c.cd(str(PROJECT_ROOT)): 744 | c.run( 745 | cmd, 746 | env=dict( 747 | UID_ENV, 748 | DOODBA_DEBUGPY_ENABLE="1", 749 | ), 750 | pty=True, 751 | ) 752 | _logger.info("Waiting for services to spin up...") 753 | time.sleep(SERVICES_WAIT_TIME) 754 | 755 | 756 | def _get_module_list( 757 | c, 758 | modules=None, 759 | core=False, 760 | extra=False, 761 | private=False, 762 | enterprise=False, 763 | only_installable=True, 764 | ): 765 | """Returns a list of addons according to the passed parameters. 766 | 767 | By default, refers to the addon from directory being worked on, 768 | unless other options are specified. 769 | """ 770 | # Get list of dependencies for addon 771 | cmd = DOCKER_COMPOSE_CMD + " run --rm odoo addons list" 772 | if core: 773 | cmd += " --core" 774 | if extra: 775 | cmd += " --extra" 776 | if private: 777 | cmd += " --private" 778 | if enterprise: 779 | cmd += " --enterprise" 780 | if modules: 781 | cmd += f" -w {modules}" 782 | if only_installable: 783 | cmd += " --installable" 784 | with c.cd(str(PROJECT_ROOT)): 785 | module_list = c.run( 786 | cmd, 787 | env=UID_ENV, 788 | pty=True, 789 | hide="stdout", 790 | ).stdout.splitlines()[-1] 791 | return module_list 792 | 793 | 794 | @task( 795 | help={ 796 | "modules": "Comma-separated list of modules to test.", 797 | "core": "Test all core addons. Default: False", 798 | "extra": "Test all extra addons. Default: False", 799 | "private": "Test all private addons. Default: False", 800 | "enterprise": "Test all enterprise addons. Default: False", 801 | "skip": "List of addons to skip. Default: []", 802 | "debugpy": "Whether or not to run tests in a VSCode debugging session. " 803 | "Default: False", 804 | "cur-file": "Path to the current file." 805 | " Addon name will be obtained from there to run tests", 806 | "mode": "Mode in which tests run. Options: ['init'(default), 'update']", 807 | "db_filter": "DB_FILTER regex to pass to the test container Set to ''" 808 | " to disable. Default: '^devel$'", 809 | }, 810 | ) 811 | def test( 812 | c, 813 | modules=None, 814 | core=False, 815 | extra=False, 816 | private=False, 817 | enterprise=False, 818 | skip="", 819 | debugpy=False, 820 | cur_file=None, 821 | mode="init", 822 | db_filter="^devel$", 823 | ): 824 | """Run Odoo tests 825 | 826 | By default, tests addon from directory being worked on, 827 | unless other options are specified. 828 | 829 | NOTE: Odoo must be restarted manually after this to go back to normal mode 830 | """ 831 | if not (modules or core or extra or private or enterprise): 832 | cur_module = _get_cwd_addon(cur_file or Path.cwd()) 833 | if not cur_module: 834 | raise exceptions.ParseError( 835 | msg="Odoo addon to install not found. " 836 | "You must provide at least one option for modules" 837 | " or be in a subdirectory of one." 838 | " See --help for details." 839 | ) 840 | modules = cur_module 841 | else: 842 | modules = _get_module_list(c, modules, core, extra, private, enterprise) 843 | odoo_command = ["odoo", "--test-enable", "--stop-after-init", "--workers=0"] 844 | if mode == "init": 845 | odoo_command.append("-i") 846 | elif mode == "update": 847 | odoo_command.append("-u") 848 | else: 849 | raise exceptions.ParseError( 850 | msg="Available modes are 'init' or 'update'. See --help for details." 851 | ) 852 | # Skip test in some modules 853 | modules_list = modules.split(",") 854 | for m_to_skip in skip.split(","): 855 | if not m_to_skip: 856 | continue 857 | if m_to_skip not in modules_list: 858 | _logger.warn( 859 | "%s not found in the list of addons to test: %s", (m_to_skip, modules) 860 | ) 861 | modules_list.remove(m_to_skip) 862 | modules = ",".join(modules_list) 863 | odoo_command.append(modules) 864 | if ODOO_VERSION >= 12: 865 | # Limit tests to explicit list 866 | # Filter spec format (comma-separated) 867 | # [-][tag][/module][:class][.method] 868 | odoo_command.extend(["--test-tags", "/" + ",/".join(modules_list)]) 869 | if debugpy: 870 | _test_in_debug_mode(c, odoo_command) 871 | else: 872 | cmd = [DOCKER_COMPOSE_CMD, "run", "--rm"] 873 | if db_filter: 874 | cmd.extend(["-e", "DB_FILTER='%s'" % db_filter]) 875 | cmd.append("odoo") 876 | cmd.extend(odoo_command) 877 | with c.cd(str(PROJECT_ROOT)): 878 | c.run( 879 | " ".join(cmd), 880 | env=UID_ENV, 881 | pty=True, 882 | ) 883 | 884 | 885 | @task( 886 | help={"purge": "Remove all related containers, networks images and volumes"}, 887 | ) 888 | def stop(c, purge=False): 889 | """Stop and (optionally) purge environment.""" 890 | cmd = DOCKER_COMPOSE_CMD + " down --remove-orphans" 891 | if purge: 892 | cmd += " --rmi local --volumes" 893 | with c.cd(str(PROJECT_ROOT)): 894 | c.run(cmd, pty=True) 895 | 896 | 897 | @task( 898 | help={ 899 | "dbname": "The DB that will be DESTROYED and recreated. Default: 'devel'.", 900 | "modules": "Comma-separated list of modules to install. Default: 'base'.", 901 | "core": "Install all core addons. Default: False", 902 | "extra": "Install all extra addons. Default: False", 903 | "private": "Install all private addons. Default: False", 904 | "enterprise": "Install all enterprise addons. Default: False", 905 | "populate": "Run preparedb task right after (only available for v11+)." 906 | " Default: True", 907 | "dependencies": "Install only the dependencies of the specified addons." 908 | "Default: False", 909 | }, 910 | ) 911 | def resetdb( 912 | c, 913 | modules=None, 914 | core=False, 915 | extra=False, 916 | private=False, 917 | enterprise=False, 918 | dbname="devel", 919 | populate=True, 920 | dependencies=False, 921 | ): 922 | """Reset the specified database with the specified modules. 923 | 924 | Uses click-odoo-initdb behind the scenes, which has a caching system that 925 | makes DB resets quicker. See its docs for more info. 926 | """ 927 | if dependencies: 928 | modules = _get_module_dependencies(c, modules, core, extra, private, enterprise) 929 | elif core or extra or private or enterprise: 930 | modules = _get_module_list(c, modules, core, extra, private, enterprise) 931 | else: 932 | modules = modules or "base" 933 | with c.cd(str(PROJECT_ROOT)): 934 | c.run(DOCKER_COMPOSE_CMD + " stop odoo", pty=True) 935 | _run = DOCKER_COMPOSE_CMD + " run --rm -l traefik.enable=false odoo" 936 | c.run( 937 | f"{_run} click-odoo-dropdb {dbname}", 938 | env=UID_ENV, 939 | warn=True, 940 | pty=True, 941 | ) 942 | c.run( 943 | f"{_run} click-odoo-initdb -n {dbname} -m {modules} --no-demo", 944 | env=UID_ENV, 945 | pty=True, 946 | ) 947 | if populate and ODOO_VERSION < 11: 948 | _logger.warn( 949 | "Skipping populate task as it is not available in v%s" % ODOO_VERSION 950 | ) 951 | populate = False 952 | if populate: 953 | preparedb(c) 954 | 955 | 956 | @task() 957 | def preparedb(c): 958 | """Run the `preparedb` script inside the container 959 | 960 | Populates the DB with some helpful config 961 | """ 962 | if ODOO_VERSION < 11: 963 | raise exceptions.PlatformError( 964 | "The preparedb script is not available for Doodba environments bellow v11." 965 | ) 966 | with c.cd(str(PROJECT_ROOT)): 967 | c.run( 968 | DOCKER_COMPOSE_CMD + " run --rm -l traefik.enable=false odoo preparedb", 969 | env=UID_ENV, 970 | pty=True, 971 | ) 972 | 973 | 974 | @task() 975 | def restart(c, quick=True): 976 | """Restart odoo container(s).""" 977 | cmd = DOCKER_COMPOSE_CMD + " restart" 978 | if quick: 979 | cmd = f"{cmd} -t0" 980 | cmd = f"{cmd} odoo odoo_proxy" 981 | with c.cd(str(PROJECT_ROOT)): 982 | c.run(cmd, env=UID_ENV, pty=True) 983 | 984 | 985 | @task( 986 | help={ 987 | "container": "Names of the containers from which logs will be obtained." 988 | " You can specify a single one, or several comma-separated names." 989 | " Default: None (show logs for all containers)" 990 | }, 991 | ) 992 | def logs(c, tail=10, follow=True, container=None): 993 | """Obtain last logs of current environment.""" 994 | cmd = DOCKER_COMPOSE_CMD + " logs" 995 | if follow: 996 | cmd += " -f" 997 | if tail: 998 | cmd += f" --tail {tail}" 999 | if container: 1000 | cmd += f" {container.replace(',', ' ')}" 1001 | with c.cd(str(PROJECT_ROOT)): 1002 | c.run(cmd, pty=True) 1003 | 1004 | 1005 | @task 1006 | def after_update(c): 1007 | """Execute some actions after a copier update or init""" 1008 | # Make custom build scripts executable 1009 | if ODOO_VERSION < 11: 1010 | files = ( 1011 | Path(PROJECT_ROOT, "odoo", "custom", "build.d", "20-update-pg-repos"), 1012 | Path(PROJECT_ROOT, "odoo", "custom", "build.d", "10-fix-certs"), 1013 | ) 1014 | for script_file in files: 1015 | # Ignore if, for some reason, the file didn't end up in the generated 1016 | # project despite of the correct version (e.g. Copier exclusions) 1017 | if not script_file.exists(): 1018 | continue 1019 | cur_stat = script_file.stat() 1020 | # Like chmod ug+x 1021 | script_file.chmod(cur_stat.st_mode | stat.S_IXUSR | stat.S_IXGRP) 1022 | else: 1023 | # Remove version-specific build scripts if the copier update didn't 1024 | # HACK: https://github.com/copier-org/copier/issues/461 1025 | files = ( 1026 | Path(PROJECT_ROOT, "odoo", "custom", "build.d", "20-update-pg-repos"), 1027 | Path(PROJECT_ROOT, "odoo", "custom", "build.d", "10-fix-certs"), 1028 | ) 1029 | for script_file in files: 1030 | # missing_ok argument would take care of this, but it was only added for 1031 | # Python 3.8 1032 | if script_file.exists(): 1033 | script_file.unlink() 1034 | 1035 | 1036 | @task( 1037 | help={ 1038 | "source_db": "The source DB name. Default: 'devel'.", 1039 | "destination_db": ( 1040 | "The destination DB name. Default: '[SOURCE_DB_NAME]-[CURRENT_DATE]'" 1041 | ), 1042 | }, 1043 | ) 1044 | def snapshot( 1045 | c, 1046 | source_db="devel", 1047 | destination_db=None, 1048 | ): 1049 | """Snapshot current database and filestore. 1050 | 1051 | Uses click-odoo-copydb behind the scenes to make a snapshot. 1052 | """ 1053 | if not destination_db: 1054 | destination_db = f"{source_db}-{datetime.now().strftime('%Y_%m_%d-%H_%M')}" 1055 | with c.cd(str(PROJECT_ROOT)): 1056 | cur_state = c.run(DOCKER_COMPOSE_CMD + " stop odoo db", pty=True).stdout 1057 | _logger.info("Snapshoting current %s DB to %s", (source_db, destination_db)) 1058 | _run = DOCKER_COMPOSE_CMD + " run --rm -l traefik.enable=false odoo" 1059 | c.run( 1060 | f"{_run} click-odoo-copydb {source_db} {destination_db}", 1061 | env=UID_ENV, 1062 | pty=True, 1063 | ) 1064 | if "Stopping" in cur_state: 1065 | # Restart services if they were previously active 1066 | c.run(DOCKER_COMPOSE_CMD + " start odoo db", pty=True) 1067 | 1068 | 1069 | @task( 1070 | help={ 1071 | "snapshot_name": "The snapshot name. If not provided," 1072 | "the script will try to find the last snapshot" 1073 | " that starts with the destination_db name", 1074 | "destination_db": "The destination DB name. Default: 'devel'", 1075 | }, 1076 | ) 1077 | def restore_snapshot( 1078 | c, 1079 | snapshot_name=None, 1080 | destination_db="devel", 1081 | ): 1082 | """Restore database and filestore snapshot. 1083 | 1084 | Uses click-odoo-copydb behind the scenes to restore a DB snapshot. 1085 | """ 1086 | with c.cd(str(PROJECT_ROOT)): 1087 | cur_state = c.run(DOCKER_COMPOSE_CMD + " stop odoo db", pty=True).stdout 1088 | if not snapshot_name: 1089 | # List DBs 1090 | res = c.run( 1091 | DOCKER_COMPOSE_CMD + " run --rm -e LOG_LEVEL=WARNING odoo psql -tc" 1092 | " 'SELECT datname FROM pg_database;'", 1093 | env=UID_ENV, 1094 | hide="stdout", 1095 | ) 1096 | db_list = [] 1097 | for db in res.stdout.splitlines(): 1098 | # Parse and filter DB List 1099 | if not db.lstrip().startswith(destination_db): 1100 | continue 1101 | db_name = db.lstrip() 1102 | try: 1103 | db_date = datetime.strptime( 1104 | db_name.lstrip(destination_db + "-"), "%Y_%m_%d-%H_%M" 1105 | ) 1106 | db_list.append((db_name, db_date)) 1107 | except ValueError: 1108 | continue 1109 | snapshot_name = max(db_list, key=lambda x: x[1])[0] 1110 | if not snapshot_name: 1111 | raise exceptions.PlatformError( 1112 | "No snapshot found for destination_db %s" % destination_db 1113 | ) 1114 | _logger.info("Restoring snapshot %s to %s", (snapshot_name, destination_db)) 1115 | _run = DOCKER_COMPOSE_CMD + " run --rm -l traefik.enable=false odoo" 1116 | c.run( 1117 | f"{_run} click-odoo-dropdb {destination_db}", 1118 | env=UID_ENV, 1119 | warn=True, 1120 | pty=True, 1121 | ) 1122 | c.run( 1123 | f"{_run} click-odoo-copydb {snapshot_name} {destination_db}", 1124 | env=UID_ENV, 1125 | pty=True, 1126 | ) 1127 | if "Stopping" in cur_state: 1128 | # Restart services if they were previously active 1129 | c.run(DOCKER_COMPOSE_CMD + " start odoo db", pty=True) 1130 | 1131 | 1132 | @task( 1133 | help={}, 1134 | ) 1135 | def update(c): 1136 | """Migrate Odoo addons""" 1137 | cmd = ( 1138 | DOCKER_COMPOSE_CMD 1139 | + " run --rm odoo click-odoo-update --watcher-max-seconds 600" 1140 | ) 1141 | with c.cd(str(PROJECT_ROOT)): 1142 | c.run( 1143 | cmd, 1144 | env=UID_ENV, 1145 | pty=True, 1146 | ) 1147 | 1148 | 1149 | @task( 1150 | help={ 1151 | "addons_dir": "Directory or specific folder path containing the addons to update POT files for. Default: odoo/custom/src", 1152 | "commit": "Whether to commit changes. Default: False", 1153 | "modules": "Comma-separated list of module patterns to match (e.g., 'spp_*,g2p_*'). Default: None (all modules)", 1154 | "database": "Database name to use. Default: devel", 1155 | "no_fuzzy": "Disable fuzzy matching. Default: False", 1156 | "update_po": "Update .po files after generating .pot files. Default: False", 1157 | "lang": "Language code for PO files to update/create. Default: None", 1158 | "force": "Force update existing PO files. Default: False", 1159 | "msgmerge": "Run msgmerge if POT file is created/updated. Default: False", 1160 | "create_i18n": "Create i18n directories if they don't exist. Default: True", 1161 | "debug": "Run in debug mode with extra logging. Default: False", 1162 | } 1163 | ) 1164 | def update_pot( 1165 | c, 1166 | addons_dir="odoo/custom/src", 1167 | commit=False, 1168 | modules=None, 1169 | database="devel", 1170 | no_fuzzy=False, 1171 | update_po=False, 1172 | lang=None, 1173 | force=False, 1174 | msgmerge=False, 1175 | create_i18n=True, 1176 | debug=False, 1177 | ): 1178 | """Update POT files and optionally PO files for all addons in specified directory or specific folder. 1179 | 1180 | IMPORTANT: Modules must be installed in the database before translations can be extracted. 1181 | The extraction process uses Odoo's translation export mechanism which requires installed modules. 1182 | 1183 | Examples: 1184 | 1. Extract POT files for specific module patterns: 1185 | invoke update-pot --modules="spp_*,g2p_*,pds_*" --database=devel --no-fuzzy --msgmerge 1186 | 1187 | 2. Update POT files and all existing PO files: 1188 | invoke update-pot --modules="spp_*,g2p_*,pds_*" --database=devel --no-fuzzy --update-po 1189 | 1190 | 3. Create/update a specific language PO file: 1191 | invoke update-pot --modules="spp_*,g2p_*,pds_*" --update-po --lang=fr 1192 | 1193 | 4. Force update existing PO files: 1194 | invoke update-pot --modules="spp_*,g2p_*,pds_*" --update-po --lang=fr --force 1195 | 1196 | 5. Process a specific module directory: 1197 | invoke update-pot --addons-dir="odoo/custom/src/openspp_modules" --database=devel --no-fuzzy 1198 | 1199 | 6. Run with debug mode for more logging: 1200 | invoke update-pot --addons-dir="odoo/custom/src/openspp_modules" --debug 1201 | """ 1202 | module_list = [] 1203 | auto_addons = Path(PROJECT_ROOT, "odoo", "auto", "addons") 1204 | addons_path = None 1205 | 1206 | if debug: 1207 | _logger.info("Running in debug mode") 1208 | _logger.info("PROJECT_ROOT: %s", PROJECT_ROOT) 1209 | _logger.info("auto_addons path: %s", auto_addons) 1210 | 1211 | if modules: 1212 | # Module pattern specified - use this directly 1213 | # Handle comma-separated patterns 1214 | patterns = [p.strip() for p in modules.split(",")] 1215 | for pattern in patterns: 1216 | if "*" in pattern: 1217 | # For patterns like spp_*, g2p_*, etc. 1218 | matching = list(auto_addons.glob(pattern)) 1219 | module_list.extend([m.name for m in matching if m.is_dir()]) 1220 | else: 1221 | # For exact module names 1222 | if (auto_addons / pattern).is_dir(): 1223 | module_list.append(pattern) 1224 | else: 1225 | # No module pattern - look at specified directory 1226 | addons_path = Path(PROJECT_ROOT, addons_dir) 1227 | if not addons_path.exists(): 1228 | # Try as absolute path 1229 | addons_path = Path(addons_dir) 1230 | if not addons_path.exists(): 1231 | raise exceptions.ParseError("Path %s does not exist" % addons_dir) 1232 | 1233 | if debug: 1234 | _logger.info("addons_path: %s", addons_path) 1235 | 1236 | # Figure out if we need to find modules in a repository or in a specific folder 1237 | if addons_path.name in ("src", "addons"): 1238 | # Path is a general container, find all repositories 1239 | for repo_path in addons_path.glob("*"): 1240 | if not repo_path.is_dir() or repo_path.name == "odoo": 1241 | continue 1242 | 1243 | # Try to get a list of modules in this repo from auto/addons 1244 | repo_name = repo_path.name 1245 | if debug: 1246 | _logger.info("Looking for modules in repo: %s", repo_name) 1247 | 1248 | for module_dir in auto_addons.glob("*"): 1249 | if not module_dir.is_dir(): 1250 | continue 1251 | 1252 | # Check if this module belongs to the repository 1253 | # We do this by checking if a link exists in auto/addons pointing to the repo 1254 | if module_dir.is_symlink(): 1255 | try: 1256 | target = module_dir.resolve() 1257 | if debug: 1258 | _logger.info( 1259 | "Module %s is a symlink to %s", 1260 | module_dir.name, 1261 | target, 1262 | ) 1263 | if repo_name in str(target): 1264 | module_list.append(module_dir.name) 1265 | except Exception as e: 1266 | _logger.warning( 1267 | "Error resolving symlink for %s: %s", module_dir, e 1268 | ) 1269 | 1270 | else: 1271 | # Path is a specific repo or module, figure out which 1272 | repo_or_module_name = addons_path.name 1273 | 1274 | if debug: 1275 | _logger.info("Examining specific path: %s", repo_or_module_name) 1276 | 1277 | # First check if it's a specific module 1278 | if (addons_path / "__manifest__.py").exists() or ( 1279 | addons_path / "__openerp__.py" 1280 | ).exists(): 1281 | # It's a module - find its name in auto/addons 1282 | module_name = repo_or_module_name 1283 | if (auto_addons / module_name).exists(): 1284 | module_list.append(module_name) 1285 | if debug: 1286 | _logger.info("Found module %s in auto/addons", module_name) 1287 | else: 1288 | # It's a repository - find all modules that belong to this repo 1289 | repo_name = repo_or_module_name 1290 | 1291 | if debug: 1292 | _logger.info("Looking for modules in repository: %s", repo_name) 1293 | 1294 | # Check the repository structure to determine the module path pattern 1295 | if (addons_path / "addons").is_dir(): 1296 | # Modules are in an 'addons' subdirectory 1297 | module_dirs = list(addons_path.glob("addons/*")) 1298 | if debug: 1299 | _logger.info( 1300 | "Found addons subdirectory with %d potential modules", 1301 | len(module_dirs), 1302 | ) 1303 | else: 1304 | # Modules are directly in the repository 1305 | module_dirs = list(addons_path.glob("*")) 1306 | if debug: 1307 | _logger.info( 1308 | "Found %d potential modules directly in repo", 1309 | len(module_dirs), 1310 | ) 1311 | 1312 | # Find corresponding modules in auto/addons 1313 | for module_dir in module_dirs: 1314 | if not module_dir.is_dir(): 1315 | continue 1316 | 1317 | module_name = module_dir.name 1318 | if (module_dir / "__manifest__.py").exists() or ( 1319 | module_dir / "__openerp__.py" 1320 | ).exists(): 1321 | if (auto_addons / module_name).exists(): 1322 | module_list.append(module_name) 1323 | if debug: 1324 | _logger.info( 1325 | "Found module %s in auto/addons", module_name 1326 | ) 1327 | 1328 | if not module_list: 1329 | # If we still don't have modules, let's try a direct path lookup from auto/addons 1330 | if addons_path: 1331 | _logger.info( 1332 | "No modules found using standard methods, trying direct path mapping..." 1333 | ) 1334 | for module_dir in auto_addons.glob("*"): 1335 | if not module_dir.is_dir(): 1336 | continue 1337 | 1338 | if module_dir.is_symlink(): 1339 | try: 1340 | target = os.path.normpath(str(module_dir.resolve())) 1341 | src_path = os.path.normpath(str(addons_path)) 1342 | 1343 | if debug: 1344 | _logger.info( 1345 | "Module %s target: %s", module_dir.name, target 1346 | ) 1347 | _logger.info("Comparing with source path: %s", src_path) 1348 | 1349 | if src_path in target or str(addons_path.name) in target: 1350 | module_list.append(module_dir.name) 1351 | if debug: 1352 | _logger.info( 1353 | "Matched module %s by path", module_dir.name 1354 | ) 1355 | except Exception as e: 1356 | _logger.warning( 1357 | "Error resolving symlink for %s: %s", module_dir, e 1358 | ) 1359 | 1360 | if not module_list: 1361 | # Last attempt: try to find any modules with repository name in their paths 1362 | if addons_path: 1363 | repo_name = addons_path.name 1364 | if debug: 1365 | _logger.info("Trying to match modules by repo name: %s", repo_name) 1366 | for module_dir in auto_addons.glob("*"): 1367 | if not module_dir.is_dir(): 1368 | continue 1369 | 1370 | if module_dir.is_symlink(): 1371 | try: 1372 | target = str(module_dir.resolve()) 1373 | if repo_name in target: 1374 | module_list.append(module_dir.name) 1375 | if debug: 1376 | _logger.info( 1377 | "Matched module %s by repo name in path", 1378 | module_dir.name, 1379 | ) 1380 | except Exception as e: 1381 | _logger.warning( 1382 | "Error resolving symlink for %s: %s", module_dir, e 1383 | ) 1384 | 1385 | if not module_list: 1386 | raise exceptions.ParseError( 1387 | "No modules found to process in %s. Try using --modules instead." 1388 | % addons_dir 1389 | ) 1390 | 1391 | _logger.info("Processing modules: %s", ", ".join(module_list)) 1392 | 1393 | # Create i18n directories if needed 1394 | if create_i18n: 1395 | for module_name in module_list: 1396 | module_path = auto_addons / module_name 1397 | i18n_dir = module_path / "i18n" 1398 | if not i18n_dir.exists(): 1399 | _logger.info("Creating i18n directory for %s", module_name) 1400 | i18n_dir.mkdir(parents=True, exist_ok=True) 1401 | 1402 | # Build the command following the pattern used by other tasks 1403 | cmd = ( 1404 | "%s run --rm odoo click-odoo-makepot --addons-dir /opt/odoo/auto/addons" 1405 | % DOCKER_COMPOSE_CMD 1406 | ) 1407 | 1408 | if database: 1409 | cmd += " -d %s" % database 1410 | if no_fuzzy: 1411 | cmd += " --no-fuzzy-matching" 1412 | if commit: 1413 | cmd += " --commit" 1414 | if msgmerge: 1415 | cmd += " --msgmerge" 1416 | 1417 | # Add modules list 1418 | cmd += " -m %s" % ",".join(module_list) 1419 | 1420 | # Add log level for more verbose output if debugging 1421 | if debug: 1422 | cmd += " --log-level=debug" 1423 | 1424 | with c.cd(str(PROJECT_ROOT)): 1425 | try: 1426 | _logger.info("Running command: %s", cmd) 1427 | c.run(cmd, env=UID_ENV, pty=True) 1428 | _logger.info("POT file generation completed") 1429 | except Exception as e: 1430 | _logger.error("Failed to update POT files: %s", e) 1431 | return 1432 | 1433 | # Check for created POT files and report 1434 | pot_files_created = [] 1435 | for module_name in module_list: 1436 | module_path = auto_addons / module_name 1437 | i18n_dir = module_path / "i18n" 1438 | pot_files = list(i18n_dir.glob("*.pot")) 1439 | if pot_files: 1440 | pot_files_created.extend([str(p) for p in pot_files]) 1441 | 1442 | if pot_files_created: 1443 | _logger.info("POT files created/updated: %d", len(pot_files_created)) 1444 | else: 1445 | _logger.warning("No POT files were created or updated. This might be because:") 1446 | _logger.warning("1. The modules have no translatable strings") 1447 | _logger.warning("2. The POT files already exist and were not changed") 1448 | _logger.warning("3. The modules are not installed in the database") 1449 | _logger.warning("4. There was an issue with the extraction process") 1450 | 1451 | # Update PO files if requested 1452 | if update_po: 1453 | _logger.info("Updating PO files") 1454 | po_files_updated = 0 1455 | # Find all i18n directories in the processed modules 1456 | i18n_dirs = [] 1457 | for module in module_list: 1458 | module_path = auto_addons / module 1459 | if (module_path / "i18n").exists(): 1460 | i18n_dirs.append(module_path / "i18n") 1461 | 1462 | for i18n_dir in i18n_dirs: 1463 | # Find all POT files 1464 | pot_files = list(i18n_dir.glob("*.pot")) 1465 | for pot_file in pot_files: 1466 | if lang: 1467 | # Update/create specific language PO file 1468 | po_file = pot_file.parent / f"{lang}.po" 1469 | if force or not po_file.exists(): 1470 | if not po_file.exists(): 1471 | # Create new PO file 1472 | _logger.info("Creating new PO file %s", po_file) 1473 | cmd = "msginit --no-translator -l %s -i %s -o %s" % ( 1474 | lang, 1475 | pot_file, 1476 | po_file, 1477 | ) 1478 | else: 1479 | # Update existing PO file 1480 | _logger.info("Updating existing PO file %s", po_file) 1481 | cmd = "msgmerge --no-fuzzy-matching -N -U %s %s" % ( 1482 | po_file, 1483 | pot_file, 1484 | ) 1485 | with c.cd(str(PROJECT_ROOT)): 1486 | try: 1487 | c.run(cmd, hide=True) 1488 | _logger.info("Updated %s", po_file) 1489 | po_files_updated += 1 1490 | except Exception as e: 1491 | _logger.error("Failed to update %s: %s", po_file, e) 1492 | else: 1493 | # Update all existing PO files 1494 | po_files = list(i18n_dir.glob("*.po")) 1495 | if not po_files: 1496 | _logger.info("No existing PO files found in %s", i18n_dir) 1497 | for po_file in po_files: 1498 | _logger.info("Updating PO file %s", po_file) 1499 | cmd = "msgmerge --update %s %s" % (po_file, pot_file) 1500 | with c.cd(str(PROJECT_ROOT)): 1501 | try: 1502 | c.run(cmd, hide=True) 1503 | _logger.info("Updated %s", po_file) 1504 | po_files_updated += 1 1505 | except Exception as e: 1506 | _logger.error("Failed to update %s: %s", po_file, e) 1507 | 1508 | _logger.info("Total PO files updated: %d", po_files_updated) 1509 | --------------------------------------------------------------------------------