#!/bin/bash cd "$(dirname "$0")" || exit # Commands such as git pull return exit code if there are no changes # set -e if [[ -a ".env" ]]; then source .env fi help-table() { local -a rows local max_cmd=7 max_opt=6 max_desc=11 rows+=("Command|Option|Description") rows+=("===ENV===| | ") rows+=("backup-env-files|[path]|Archive and copy ,env files to target location.") rows+=("copy-dotenv|[env][env]|Copy .env file.") rows+=("create-dotenv|[env][type]|Create .env file of type nextcloud, odoo, upgrade.") rows+=("edit-dotenv|[env]|Open .env file in default editor.") rows+=("list-dotenv||List .env files.") rows+=("load-dotenv|[env]|Load and export .env file.") rows+=("remove-dotenv|[env]|Remove environment config.") rows+=("rename-dotenv|[env][env]|Rename .env file.") rows+=("restore-dotenv-files|[path]|Extract and copy .env files from backup file.") rows+=("show-dotenv|[env]|Output content of the .env file.") rows+=("===GIT===| | ") rows+=("aggregate-git-folders|[path]|Run gitaggregate for the repos.yaml.") rows+=("add-git-folder|[branch][url][path]|Add git folder to .gitmodules and .gitmodule.csv.") rows+=("checkout|[version]|Checkout Odoo version.") rows+=("checkout-git-folder|[branch][path]|Checkout git commit from .gitmodules.csv.") rows+=("commit-git-folder|[message][path]|Commit all changes in path.") rows+=("clean-git-folder|[path]|Clean git folder.") rows+=("clone-git-folder|[branch][path][version]|Clone git folder listed in the .gitmodules file.") rows+=("download-git-folder|[grep]|Download git folder listed in the .gitmodules file.") rows+=("status-git-folder|[path]|Show status for git folder in path.") rows+=("list-git-folder|[path]|List path and url of git folders.") rows+=("pull-git-folder|[branch][path]|Pull all git folders listed in the .gitmodules file.") rows+=("push-git-folder||Push all git folders in path.") rows+=("remove-git-folder|[path]|Remove git folders in path.") rows+=("ls-git-folder|[grep]|List git folders path space separated.") rows+=("reset-git-folder|[path]|Abort rebase and reset git folder.") rows+=("stage-git-folder|[path]|Stage all files in git folders in path.") rows+=("switch-git-folder|[branch][path]|Switch branch for all git folders from the .gitmodules file.") rows+=("sync-git-folder|[branch][path]|Switch, stash and pull all git folders.") rows+=("run-pre-commit|[path]|Run pre-commit in path.") rows+=("===DOCKER===| | ") rows+=("archive-docker-tags||Archive Docker image tags on hub older than 1 year.") rows+=("build-odoo|[output]|Build Odoo Docker image.") rows+=("build-odoo-mailgate|[output]|Build Odoo Mailgate Docker image.") rows+=("build-odoo-upgrade|[output]|Build Odoo Upgrade Docker image.") rows+=("build-odoo-cli|[output]|Build Odoo CLI Docker image.") rows+=("login-docker|[user][token]|Setup Docker Hub login credentials.") rows+=("login-podman|[user][token]|Login into registry with podman.") rows+=("container-ps||List container processes.") rows+=("remove|[name]|Remove containers and volumes.") rows+=("test-image|[folder]|Test Docker image from target folder.") rows+=("test-odoo-version-regex||Test the Odoo module version regex filter.") rows+=("===VUEPRESS===| | ") rows+=("build-vuepress||Create Vuepress build.") rows+=("install-vuepress||Install Node build dependencies.") rows+=("dev-vuepress||Start Vuepress development server.") rows+=("serve-vuepress||Serve Vuepress build.") rows+=("===DATABASE===| | ") rows+=("change-uuid|[env]|Change database uuid via xmlrpc.") rows+=("clear-assets|[db]|Clear all assets of Odoo database.") rows+=("clear-filestore|[db]|Clear local filestore folder.") rows+=("clear-views|[db]|Clear all views of Odoo database.") rows+=("cloc-odoo|[db]|Count custom line of codes.") rows+=("drop-db|[db]|Drop target Odoo database.") rows+=("init-db|[db]|Initialize the Odoo database.") rows+=("load-language|[db][lang]|Install language package in Odoo db.") rows+=("patch-database|[db][path]|Apply sql file to database.") rows+=("reset-views|[db][key]|Execute hard reset on views matching keys.") rows+=("set-admin|[db]|Sets the password for the first user in database.") rows+=("update-module|[db][name,path]|Update target Odoo module.") rows+=("update-module-list|[db]|Update module list of Odoo database.") rows+=("===REVISION===| | ") rows+=("checkout-latest-revision|[version]|Checkout the latest revision of the Odoo version.") rows+=("checkout-revision|[revision]|Load Odoo revision env var and checkout git folders.") rows+=("commit-and-push-revision|[revision]|Commit all changes and tag with current revision.") rows+=("create-revision|[revision]|Create new Odoo revision.") rows+=("list-revision||List available Odoo revisions.") rows+=("load-latest-revision|[version]|Load the latest revision of the Odoo version.") rows+=("load-revision|[revision]|Load env var from specified revision.") rows+=("show-revision|[revision]|Show references of Odoo revision.") rows+=("===LLM===| | ") rows+=("commit-with-llm||Commit with llm generated commit message.") rows+=("update-with-llm|[glob][prompt]|Feed module files with prompt to LLM and apply file changes.") rows+=("===SNIPPETS===| | ") rows+=("create-snippet|[xml-id]|Create snippet from template.") rows+=("install-snippet|[env][path]|Install snippet definition.") rows+=("disable-snippet|[env][path]|Disable snippet by path or xml-id.") rows+=("enable-snippet|[env][path]|Enable snippet by path or xml-id.") rows+=("lint-snippets||Run checks for all snippets.") rows+=("remove-snippet|[env][path]|Remove snippet by path or xml-id.") rows+=("update-snippet|[env][path]|Update snippet definition.") rows+=("===INSTALL===| | ") rows+=("activate-venv||Activate virtualenv.") rows+=("init-venv||Initialize python virtual env.") rows+=("disable-mailserver|[env]|Disable mail server settings via xmlrpc.") rows+=("install||Install Odoo requirements in source folder.") rows+=("install-ansible-build-scripts|[role]|Install scripts of the specified Ansbile role.") rows+=("install-requirements|[db][path]|Install python packages from requirements.txt.") rows+=("list-packages||List installed python packages.") rows+=("init-module|[db][path,module]|Initialize Odoo module.") rows+=("set-addons-path||Set Odoo addons path env variable.") rows+=("===DATA===| | ") rows+=("export-website-data|[env]|Export website data from Odoo database.") rows+=("import-csv|[db][path]|Import data from csv. Filename is PostgreSQL table name.") rows+=("import-website-data|[env]|Import website data to Odoo database.") rows+=("===DEVELOP===| | ") rows+=("create-module|[path]|Create new Odoo module from template.") rows+=("create-repo|[path]|Odoo module repo from template.") rows+=("generate-module-docs|[path]|Generate readme file for module with OCA tools.") rows+=("generate-module-model|[path][model]|Generate model in module folder.") rows+=("generate-module-inherit|[path][model]|Generate inherited model in module folder.") rows+=("generate-module-views|[path][model]|Generate model views in module folder.") rows+=("generate-module-security|[path][model]|Generate model access file.") rows+=("generate-module-migration|[path]|Generate migration script for currentmodule version.") rows+=("generate-module-snippet|[path][ref][model]|Generate snippet for referefenced view.") rows+=("generate-module-report|[path][model]|Generate report for a model in module folder.") rows+=("generate-module-translation|[path]|Generate module translation and update with llm.") rows+=("generate-module-wizard|[path][model]|Generate wizard for a model in module folder.") rows+=("generate-repo-docs|[path]|Generate repo files from template.") rows+=("===MODULE===| | ") rows+=("list-modules|[path]|Get modules in path as bash array.") rows+=("pytest-module|[path]|Run module tests with pytest.") rows+=("release-module|[path]|Create GitHub release for a module.") rows+=("remove-module|[db][name]|Remove target Odoo module.") rows+=("test-module|[path]|Test target Odoo module.") rows+=("test-modules|[path]|Test target Odoo modules in repo folder.") rows+=("translate-module|[path][lang][db]|Generate translation for Odoo module.") rows+=("upload-module|[env][path]|Zip and upload Odoo module.") rows+=("visualize-dependencies|[path]|Generate visualizations of module dependencies.") rows+=("zip-module|[path]|Create zip file for module.") rows+=("===HELPER===| | ") rows+=("generate-admin-password|[pass]|Generate hash for Odoo master password.") rows+=("generate-ssh-keys||Generate ssh key pair.") rows+=("generate-pg-ssl-keys||Generate PostgreSQL SSL key material.") rows+=("get-addons-path||Output addons path.") rows+=("get-modules|[path][option]|Get list of modules in path. 'basename' or 'dirname'.") rows+=("get-module-version|[path]|Get module version from manifest.") rows+=("get-module-checksum|[path]|Get module checksum.") rows+=("help|[grep]|Show help for commands.") rows+=("info||Show values of project env vars.") rows+=("load-ssh-key||Load SSH private key from env var.") rows+=("update-module-license|[grep]|Update LICENSE file for each matching module.") rows+=("===RUN===| | ") rows+=("debug|[name]|Debugg application.") rows+=("exec|[name][cmd]|Run command in container.") rows+=("logs|[name]|Tail container logs. Default is '\''odoo'\''.") rows+=("manifestoo|[param]|Execute manifestoo cli.") rows+=("odoobin|[param]|Execute odoo binary.") rows+=("odoocli|[param]|Execute odoocli cli.") rows+=("psql|[db]|Start interactive psql shell.") rows+=("restart|[name]|Restart container.") rows+=("run|[name][cmd]|Run container with command.") rows+=("shell|[db][code]|Start interactive odoo shell or run code.") rows+=("source||Source the Python virtual env.") rows+=("start|[name][db]|Start service. Use list to show services.") rows+=("stop|[name]|Stop containers.") rows+=("test-xmlrpc|[env]|Test xml rpc connection.") rows+=("===PROJECT===| | ") rows+=("lint||Run precommit for this project.") rows+=("list-versions||List available Odoo versions.") rows+=("check-version||Check if Odoo source is the correct version.") rows+=("load-version|[version]|Load git refs from version folder.") rows+=("save-version||Save git folder refs to version folder.") rows+=("template-compose||Template the Docker compose file.") rows+=("template-odoo-rc||Template the Odoo config file.") rows+=("test-project|[cleanup]|Run tests for this project.") rows+=("version||Show version of required tools.") rows+=("===PERFORMANCE===| | ") rows+=("record-with-memray|[name]|Record application memory usage with memray.") rows+=("record-with-py-spy|[pid]|Record and create flamechart for a process.") rows+=("===DOC===| | ") rows+=("update-docs||Update all project docs.") rows+=("update-modules-doc||Update modules docs file.") rows+=("update-revisions-doc||Update revisions doc file.") rows+=("update-snippets-doc||Update snippets doc file.") rows+=("===UPGRADE===| | ") rows+=("prepare-migration-brach|[path][module][version]|Create migration branch for a module.") rows+=("migrate-module|[path]|Migrate module code from to target Odoo version.") rows+=("upgrade-odoo|[env][step]|Run the Odoo upgrade for a target environment.") for row in "${rows[@]}"; do IFS='|' read -r cmd opt desc <<< "$row" (( ${#cmd} > max_cmd )) && max_cmd=${#cmd} (( ${#opt} > max_opt )) && max_opt=${#opt} (( ${#desc} > max_desc )) && max_desc=${#desc} done printf '| %-*s | %-*s | %-*s |\n' \ "$max_cmd" "Command" \ "$max_opt" "Option" \ "$max_desc" "Description" printf '|-%*s-|-%*s-|-%*s-|\n' \ "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \ "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \ "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')" local first_row=true for row in "${rows[@]:1}"; do IFS='|' read -r cmd opt desc <<< "$row" if [[ "$cmd" =~ ^===.*===$ ]]; then group_name="${cmd/===/}" group_name="${group_name/===/}" if [ "$first_row" = true ]; then first_row=false else printf '|-%*s-|-%*s-|-%*s-|\n' \ "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \ "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \ "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')" fi local left_pad=$(( (max_cmd - ${#group_name}) / 2 )) local right_pad=$(( max_cmd - left_pad - ${#group_name} )) printf '| %*s%s%*s | %*s | %*s |\n' \ "$left_pad" "" \ "$group_name" \ "$right_pad" "" \ "$max_opt" "" \ "$max_desc" "" printf '|-%*s-|-%*s-|-%*s-|\n' \ "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \ "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \ "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')" else first_row=false printf '| %-*s | %-*s | %-*s |\n' \ "$max_cmd" "$cmd" \ "$max_opt" "$opt" \ "$max_desc" "$desc" fi done } help() { echo if [[ -n "$1" ]]; then help-table | grep -i "$1" | column -t -s'|' else echo 'task [options]' echo echo 'commands:' echo help-table fi echo } # Static env vars ODOO_RC="odoo.conf" TASK_DOTENV_DIR="$PWD/vault" export PYTHONPATH="$PWD/odoo" # Required so upgrade-util does not overwrite the odoo namespace ODOO_VERSION=$(echo "$ODOO_REVISION" | cut -d'.' -f1-2) CONTAINER_CONFIG="$HOME/.docker/$CONTAINER_REGISTRY" # Updateable env vars USER_ORG=${USER_ORG:=Mint-System} DB_NAME=${DB_NAME:="$ODOO_VERSION"} WITHOUT_DEMO=${WITHOUT_DEMO:=False} ODOO_LANGUAGE=${ODOO_LANGUAGE:=de_CH} ODOO_PORT=${ODOO_PORT:=8069} NGINX_PORT=${NGINX_PORT:=8080} WORKERS=${WORKERS:=0} POSTGRES_IMAGE=${POSTGRES_IMAGE:=postgres:16-alpine} POSTGRES_PORT=${POSTGRES_PORT:=5432} POSTGRES_SSL=${POSTGRES_SSL:=on} PGSSLMODE=${PGSSLMODE:=prefer} LOG_LEVEL=${LOG_LEVEL:=info} CONTAINER_REGISTRY=${CONTAINER_REGISTRY:=mintsystem} PLATFORM=${PLATFORM:=linux/amd64} CONTAINER_ENGINE=${CONTAINER_ENGINE:=docker} COMPOSE_COMMAND=${COMPOSE_COMMAND:=docker compose} LLM_MODEL=${LLM_MODEL:=llama} SMTP_SERVER=${SMTP_SERVER:=localhost} SMTP_PORT=${SMTP_PORT:=1025} SMTP_SSL=${SMTP_SSL:=False} EMAIL_FROM=${EMAIL_FROM:=info@yourcompany.com} MAIL_ALIAS_DOMAIN=${MAIL_ALIAS_DOMAIN:=yourcompany.com} LIST_DB=${LIST_DB:=False} CONTAINER_TAG="odoo:$ODOO_REVISION" ODOO_INIT_LOGIN=${ODOO_INIT_LOGIN:=admin} ODOO_INIT_PASSWORD=${ODOO_INIT_PASSWORD:=admin} SNIPPET_PREFIX=${SNIPPET_PREFIX:=mint_system} # Conditional env vars if [[ "$CONTAINER_ENGINE" == "podman" ]]; then COMPOSE_COMMAND="podman-compose" fi if [[ "$(uname)" == "Darwin" ]]; then OS_RELEASE="Darwin" PYTHON_NOTIFY="" else OS_RELEASE=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') PYTHON_NOTIFY="inotify" fi if [[ "$WITHOUT_DEMO" = "True" ]]; then ODOO_PARAM="--without-demo=all" elif [[ "$ODOO_VERSION" = "19.0" ]]; then ODOO_PARAM="--with-demo" fi # Import commands clone-taskfile(){ if [[ ! -d "$HOME/taskfile.build" ]]; then echo -e '\033[38;5;214mGit\033[0m: Clone taskfile repo' git clone https://git.taskfile.build "$HOME/taskfile.build" else echo -e '\033[38;5;214mGit\033[0m: Pull taskfile repo' git -C "$HOME/taskfile.build" pull fi } if [[ -d "$HOME/taskfile.build/bin" ]]; then for file in "$HOME/taskfile.build/bin/"*; do if [[ -f "$file" ]]; then source "$file" fi done fi # Help commands info() { set-addons-path echo "OS Release: $OS_RELEASE" echo "Odoo Revision: $ODOO_REVISION" echo "Odoo Version: $ODOO_VERSION" echo "Odoo Port: $ODOO_PORT" echo "Postgres Port: $POSTGRES_PORT" echo "Odoo Language: $ODOO_LANGUAGE" echo "Odoo Param: $ODOO_PARAM" echo "Container Registry: $CONTAINER_REGISTRY" echo "Container Config: $CONTAINER_CONFIG" echo "Addons Path: $ADDONS_PATH" } version() { uv --version activate-venv python -V wkhtmltopdf -V if [[ "$CONTAINER_ENGINE" == "docker" ]]; then docker -v docker compose version else podman --version podman-compose --version fi } generate-admin-password() { if [[ -z "$1" ]]; then echo '\$1 is empty.'; exit; fi export ODOO_PASSWORD="$1" bin/hash-password } # Env commands init-venv() { echo "Ensure Python version $(cat .python-version) is installed." uv python install if [[ ! -d ".venv$ODOO_VERSION" ]]; then echo "Init .venv$ODOO_VERSION with $(uv --version)." uv venv ".venv$ODOO_VERSION" fi } activate-venv() { if [[ -f ".venv$ODOO_VERSION/bin/activate" ]]; then source ".venv$ODOO_VERSION/bin/activate" else echo "Could not find .venv$ODOO_VERSION/bin/active. Use init-venv to initalize." fi } get-addons-path() { set-addons-path echo "$ADDONS_PATH" } template-odoo-rc() { echo "Template $ODOO_RC" # Load and export vars defined in odoo conf template for var in $(grep -oE '\$([A-Z_][A-Z0-9_]*)|\$\{([A-Z_][A-Z0-9_]*)\}' odoo.conf.template); do var=${var/\$/} var=${var//\{\//} export $var done local ir_config_parameter=${IR_CONFIG_PARAMETER:-""} if [[ -n "$GIT_SSH_PRIVATE_KEY" ]]; then local git_ssh_private_key=$(echo -e "$GIT_SSH_PRIVATE_KEY" | base64 -w0) ir_config_parameter+=$'\n'"git.ssh_private_key = \"$git_ssh_private_key\"" fi if [[ -n "$GIT_SSH_PUBLIC_KEY" ]]; then ir_config_parameter+=$'\n'"git.ssh_public_key = \"$GIT_SSH_PUBLIC_KEY\"" fi if [[ -n "$BASE_URL_WEBSITE" ]]; then ir_config_parameter+=$'\n'"base_url_website = \"$BASE_URL_WEBSITE\"" fi if [[ -n "$BASE_URL_APP" ]]; then ir_config_parameter+=$'\n'"base_url_app = \"$BASE_URL_APP\"" fi if [[ -n "$UPLOADCARE_PUBLIC_KEY" ]]; then ir_config_parameter+=$'\n'"uploadcare.public_key = \"$UPLOADCARE_PUBLIC_KEY\"" fi if [[ -n "$UPLOADCARE_SECRET_KEY" ]]; then ir_config_parameter+=$'\n'"uploadcare.secret_key = \"$UPLOADCARE_SECRET_KEY\"" fi if [[ -n "$KEYCLOAK_BASE_URL" ]]; then ir_config_parameter+=$'\n'"keycloak.base_url = \"$KEYCLOAK_BASE_URL\"" fi if [[ -n "$KEYCLOAK_CLIENT_ID" ]]; then ir_config_parameter+=$'\n'"keycloak.client_id = \"$KEYCLOAK_CLIENT_ID\"" fi if [[ -n "$KEYCLOAK_CLIENT_SECRET" ]]; then ir_config_parameter+=$'\n'"keycloak.client_secret = \"$KEYCLOAK_CLIENT_SECRET\"" fi if [[ -n "$KEYCLOAK_REALM" ]]; then ir_config_parameter+=$'\n'"keycloak.realm = \"$KEYCLOAK_REALM\"" fi export IR_CONFIG_PARAMETER="$ir_config_parameter" envsubst < "odoo.conf.template" > "$ODOO_RC" } set-addons-path() { # Read addons path from git module local git_folders_path="$(git config --file .gitmodules --get-regexp path | awk '{ print $2 }' | sort | tr '\n' ',' | sed 's/,*$//')" # Remove excluded addons local excludes="addons/theme_mint_system addons/company addons/web" for exclude in $excludes; do git_folders_path=$(echo "$git_folders_path" | sed "s|${exclude},||g; s|^${exclude}$||") done # Filter valid directories local valid_paths="" while IFS= read -r addon; do addon=$(echo "$addon" | xargs) [[ -z "$addon" ]] && continue if [[ -d "$addon" ]]; then valid_paths="${valid_paths:+$valid_paths,}$addon" else echo "Set Addons Path: Path '$addon' does not exist." >&2 fi done < <(echo "$git_folders_path" | tr ',' '\n') git_folders_path="$valid_paths" # Convert to source paths (add /addons where needed) local addons_path="$(echo "$git_folders_path" | sed 's|,odoo|,odoo/addons|g')" # Append paths from config env var if [[ -n "$ODOO_ADDONS_PATH" ]]; then addons_path="${ODOO_ADDONS_PATH},${addons_path}" fi # Export the addons path export ADDONS_PATH="$addons_path" } load-ssh-key() { if [[ -n "$GIT_SSH_PRIVATE_KEY" ]]; then echo 'Setup SSH key from env var.' local decoded_git_ssh_private_key=$(echo -e "$GIT_SSH_PRIVATE_KEY" | base64 -d) mkdir -p ~/.ssh echo "$decoded_git_ssh_private_key" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519 || (echo 'Dumping ~/.ssh/id_ed25519 content:' && cat ~/.ssh/id_ed25519) else echo 'The SSH key env var is empty.' exit 1 fi } # Docs commands lint() { echo "Run pre-commit in $PWD" pre-commit run echo "Run snippet linting" lint-snippets } update-snippets-doc() { ./bin/update-snippets-doc } update-revisions-doc() { echo 'Update revisions doc file.' rm -f "revisions.md" echo -e "# Odoo Revisions \n\ A Odoo revision is a snapshot of git references of the Odoo source and modules at a specific date.\n\ For each major release there are multiple revisions.\n" > "revisions.md" declare -A revisions local path_url="https://github.com/${USER_ORG}/Odoo-Build/tree/main" for file in $(ls revisions/ | sort -rV); do local revision="$file" local version=$(echo "$revision" | cut -d'.' -f1-2) if [[ -z "${revisions[$version]}" ]]; then revisions[$version]="## $version" fi revisions[$version]+=$'\n\n' revisions[$version]+="#### $revision" revisions[$version]+=$'\n\n```bash\n' revisions[$version]+=$(cat "revisions/$file") revisions[$version]+=$'\n```' done for revision in $(echo "${!revisions[@]}" | tr ' ' '\n' | sort -rV); do echo -e "${revisions[$revision]}\n" >> "revisions.md" done } update-modules-doc() { echo 'Update module doc file.' ./bin/update-modules-doc } update-docs() { update-modules-doc update-revisions-doc update-snippets-doc } update-module-license() { if [[ -z "$1" ]]; then echo '\$1 is empty.'; exit; fi local git_folders=$(list-module "$1") for git_folder in $git_folders; do echo "Update license file for $git_folder." cp templates/module/LICENSE "$git_folder/" done } generate-repo-docs(){ if [[ -z "$1" ]]; then echo '\$1 is empty.'; exit; fi local repo_path="$1" echo "Copy task file to $repo_path." cp "templates/task" "$repo_path" echo "Template README.md to $repo_path." export REPO_FOLDER=$(basename "$repo_path") export REPO_MODEL=${REPO_FOLDER%%_*} export REPO_TITLE=$(echo "$REPO_FOLDER" | sed 's/_/ /g' | sed 's/\b\(.\)/\u\1/g') export REPO_NAME=$(echo "$REPO_FOLDER" | sed 's/_/-/g' | sed 's/\b\(.\)/\u\1/g') envsubst < "templates/README.md" > "$repo_path/README.md" echo "Copy $ODOO_VERSION template files to $repo_path." cp -r "templates/$ODOO_VERSION/." "$repo_path" echo 'Remove deprecated repo files.' rm -f "$repo_path/.flake8" rm -f "$repo_path/.isort.cfg" rm -f "$repo_path/.pylintrc-mandatory" } # Module commands get-modules() { if [[ -z "$1" ]]; then echo '\$1 is empty.'; exit; fi local option="$2" if [[ -z "$option" ]]; then option="dirname" fi local modules if [[ "$option" == "dirname" ]]; then modules=$(echo "$1" | tr "," "\n" | xargs -I {} find {} -type f -name "__manifest__.py" | xargs grep -l -iE "version.*([0-9]+\.[0-9]+(\.[0-9]+)?|${ODOO_VERSION}(\.[0-9]+)*)" | xargs -r dirname | sort -u | tr "\n" "," | sed 's/,$//') fi if [[ "$option" == "basename" ]]; then modules=$(echo "$1" | tr "," "\n" | xargs -I {} find {} -type f -name "__manifest__.py" | xargs grep -l -iE "version.*([0-9]+\.[0-9]+(\.[0-9]+)?|${ODOO_VERSION}(\.[0-9]+)*)" | xargs -r dirname | xargs -r -I {} basename {} | sort -u | tr "\n" "," | sed 's/,$//') fi echo "$modules" } get-module-version() { if [[ -z "$1" ]]; then echo '\$1 is empty.' else # Get version of module local version=$(python -c "import ast; print(ast.literal_eval(open('$1/__manifest__.py').read())['version'])") # Set default version [[ -z "$version" ]] && version=0.0 local count_dots=$(echo "$version" | grep -o "\." | wc -l) # Check if oca version or enterprise version if [[ $count_dots == 2 ]]; then version="$ODOO_VERSION.$version" fi if [[ $count_dots == 1 ]]; then version="$ODOO_VERSION.$version" fi echo "$version" fi } get-module-checksum() { if [[ -z "$1" ]]; then echo '\$1 is empty.' else bin/get-module-checksum "$1" fi } list-modules() { if [[ -z "$1" ]]; then echo '\$1 is empty.'; exit; fi local modules=$(get-modules "$1" dirname) echo "$modules" | tr ',' '\n' } # Container commands archive-docker-tags() { export CONTAINER_CONFIG python bin/archive-docker-tags } template-compose() { echo 'Template compose.yml' # Load and export vars defined in compose template for var in $(grep -oE '\$\{?[A-Z_][A-Z0-9_]*\}?' compose.yml.template); do # Strip $, ${, } var=${var#\$\{} # Remove leading ${ var=${var%\}} # Remove trailing } var=${var#\$} # Remove leading $ export "$var" done # Add volume mounts VOLUME_MOUNTS="" if [[ -d "./tmp/postgres-odoo" ]]; then VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./tmp/postgres-odoo:/mnt/postgres-odoo"$'\n' fi if [[ -d "./addons" ]]; then VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./addons:/mnt/addons"$'\n' fi if [[ -d "./oca" ]]; then VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./oca:/mnt/oca"$'\n' fi if [[ -d "./enterprise" ]]; then VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./enterprise:/mnt/enterprise"$'\n' fi if [[ -d "./themes" ]]; then VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./themes:/mnt/themes"$'\n' fi VOLUME_MOUNTS="${VOLUME_MOUNTS%$'\n'}" export VOLUME_MOUNTS envsubst < "compose.yml.template" > "compose.yml" } container-ps() { $CONTAINER_ENGINE ps } build-odoo() { odoo_date=$(echo "$ODOO_REVISION" | cut -d'.' -f3-) source "revisions/$ODOO_REVISION" manifest_file="images/odoo/Dockerfile" container_tag="odoo:$ODOO_VERSION" container_revision_tag="odoo:$ODOO_REVISION" case "$ODOO_VERSION" in "15.0") local python_version="3.9" PLATFORM="linux/amd64" ;; "16.0") local python_version="3.11" ;; "17.0") local python_version="3.11" ;; "18.0") local python_version="3.12" ;; "19.0") local python_version="3.13" ;; *) local python_version="3.12" ;; esac local docker_output="--load" if [[ -n "$1" ]]; then docker_output="$1" fi echo "Python Version: $python_version" echo "Platform: $PLATFORM" echo "Enterprise Ref: $ODOO_ENTERPRISE_REF" echo "Docker Output: $docker_output" echo "Remove container image with tag ${CONTAINER_REGISTRY}/${container_revision_tag}" docker image rm -f "${CONTAINER_REGISTRY}/${container_revision_tag}" if ! docker buildx inspect odoo_builder >/dev/null 2>&1; then docker buildx create --name odoo_builder --use docker buildx inspect --bootstrap else docker buildx use odoo_builder fi echo "Run Docker build ${CONTAINER_REGISTRY}/${container_revision_tag} for ${PLATFORM}" SOURCE_DATE_EPOCH=$(git -C odoo log -1 --pretty=%ct) docker buildx build --platform "$PLATFORM" . \ --file "$manifest_file" \ --build-arg PYTHON_VERSION="$python_version" \ --build-arg ODOO_VERSION="$ODOO_VERSION" \ --build-arg ODOO_DATE="$odoo_date" \ --build-arg ODOO_ENTERPRISE_REF="$ODOO_ENTERPRISE_REF" \ --tag "${CONTAINER_REGISTRY}/${container_tag}" \ --tag "${CONTAINER_REGISTRY}/${container_revision_tag}" \ "$docker_output" } build-odoo-cli() { manifest_file="images/odoo-cli/Dockerfile" container_tag="odoo-cli:latest" local docker_output="--load" if [[ -n "$1" ]]; then docker_output="$1" fi echo "Platform: $PLATFORM" echo "Docker Output: $docker_output" echo "Remove container image with tag ${CONTAINER_REGISTRY}/${container_tag}" docker image rm -f "${CONTAINER_REGISTRY}/${container_tag}" if ! docker buildx inspect odoo_builder >/dev/null 2>&1; then docker buildx create --name odoo_builder --use docker buildx inspect --bootstrap else docker buildx use odoo_builder fi echo "Run Docker build ${CONTAINER_REGISTRY}/${container_tag} for ${PLATFORM}" SOURCE_DATE_EPOCH=$(git -C odoo log -1 --pretty=%ct) docker buildx build --platform "$PLATFORM" . \ --file "$manifest_file" \ --tag "${CONTAINER_REGISTRY}/${container_tag}" \ "$docker_output" } build-odoo-mailgate() { manifest_file="images/odoo-mailgate/Dockerfile" container_tag="odoo-mailgate:latest" local docker_output="--load" if [[ -n "$1" ]]; then docker_output="$1" fi echo "Platform: $PLATFORM" echo "Docker Output: $docker_output" echo "Remove container image with tag ${CONTAINER_REGISTRY}/${container_tag}" docker image rm -f "${CONTAINER_REGISTRY}/${container_tag}" if ! docker buildx inspect odoo_builder >/dev/null 2>&1; then docker buildx create --name odoo_builder --use docker buildx inspect --bootstrap else docker buildx use odoo_builder fi echo "Run Docker build ${CONTAINER_REGISTRY}/${container_tag} for ${PLATFORM}" SOURCE_DATE_EPOCH=$(git -C odoo log -1 --pretty=%ct) docker buildx build --platform "$PLATFORM" . \ --file "$manifest_file" \ --tag "${CONTAINER_REGISTRY}/${container_tag}" \ "$docker_output" } build-odoo-upgrade() { local manifest_file="images/odoo-upgrade/Dockerfile" local postgres_versions="14 15 16 17" local docker_output="--load" if [[ -n "$1" ]]; then docker_output="$1" fi if ! docker buildx inspect odoo_builder >/dev/null 2>&1; then docker buildx create --name odoo_builder --use docker buildx inspect --bootstrap else docker buildx use odoo_builder fi for postgres_version in $postgres_versions; do container_tag="odoo-upgrade:$postgres_version" echo "Posgres Version: $postgres_version" echo "Platform: $PLATFORM" echo "Docker Output: $docker_output" echo "Removing existing image: $full_tag" docker image rm -f "$full_tag" 2>/dev/null || true echo "Running Docker build for $full_tag on platform $PLATFORM" SOURCE_DATE_EPOCH=$(git -C odoo log -1 --pretty=%ct) \ docker buildx build --platform "$PLATFORM" . \ --file "$manifest_file" \ --build-arg POSTGRES_VERSION="$postgres_version" \ --tag "${CONTAINER_REGISTRY}/${container_tag}" \ "$docker_output" if [[ $? -ne 0 ]]; then echo "Build failed for PostgreSQL $postgres_version" return 1 fi echo "Build succeeded for PostgreSQL $postgres_version" done } load-latest-revision() { if [[ -n "$1" ]]; then ODOO_VERSION="$1" fi ODOO_REVISION=$(list-revision | grep "$ODOO_VERSION" | tail -n1) load-revision "$ODOO_REVISION" } checkout-latest-revision() { if [[ -n "$1" ]]; then ODOO_VERSION="$1" fi ODOO_REVISION=$(list-revision | grep "$ODOO_VERSION" | tail -n1) checkout-revision "$ODOO_REVISION" check-version } container-login() { if [[ -n "$1" ]]; then CONTAINER_REGISTRY_USERNAME="$1" fi if [[ -n "$2" ]]; then CONTAINER_REGISTRY_PASSWORD="$2" fi echo "Docker login with username $CONTAINER_REGISTRY_USERNAME." echo "$CONTAINER_REGISTRY_PASSWORD" | docker --config "$CONTAINER_CONFIG" login --username "$CONTAINER_REGISTRY_USERNAME" --password-stdin echo "$CONTAINER_REGISTRY_PASSWORD" | docker --config "$CONTAINER_CONFIG" login dhi.io --username "$CONTAINER_REGISTRY_USERNAME" --password-stdin } login-podman() { if [[ -n "$1" ]]; then CONTAINER_REGISTRY_USERNAME="$1" fi if [[ -n "$2" ]]; then CONTAINER_REGISTRY_PASSWORD="$2" fi echo "Podman login with username $CONTAINER_REGISTRY_USERNAME." podman login --username "$CONTAINER_REGISTRY_USERNAME" --password "$CONTAINER_REGISTRY_PASSWORD" docker.io } test-odoo-version-regex() { # If ODOO_VERSION=19.0 then # 1.0 Match # 1.1 Match # 2.5 Match # 2.0.0 Match # 13.0.1.0.0 No match # 14.0.2.1.1 No match # 13.0.1.0.0 No match # 19.0.0.0.0 Match # Variations of the version key: # 'version': '19.0.x', # "version": "1.0", # 'version': '19.0.1.0.0', echo -e "\033[38;5;214mTEST\033[0m: Find and check versions in module" versions=$(grep -r --include="__manifest__.py" '[\"'\'']version[\"'\'']:.[\"'\'']' addons enterprise oca odoo themes | sed -E 's/.+[\"'\'']version[\"'\'']:.[\"'\'']([0-9].+)[\"'\''].+/\1/g' | sort -u) ODOO_VERSION="19.0" for version in $versions; do echo -n "Check version $version: " # 1. regex: version.[0-9]\.[0-9]$ # 2. regex: version.[0-9]\.[0-9]\.[0-9]$ # 3. regex: version.${ODOO_VERSION}\.[0-9]\.[0-9]\.[0-9]$ if [[ $version =~ ^[0-9]+\.[0-9]+$ || $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ || $version =~ ^${ODOO_VERSION}\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "✓ Match" else echo "✗ No match" fi done echo -e "\033[38;5;214mTEST\033[0m: Run set-addons-path" export PATH=./images/odoo/bin/:$PATH export TEST_ADDONS_DIR="./templates,./themes,./enterprise,/gugus,./oca,./addons" export ODOO_VERSION="19.0" source ./images/odoo/bin/set-addons-path echo "ADDONS_PATH: $ADDONS_PATH" } test-image() { # Test the scripts of the Docker images without starting the enviroment. set -e if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ "$1" == "images/odoo-upgrade" ]]; then container_tag="odoo-upgrade" elif [[ "$1" == "images/odoo-cli" ]]; then container_tag="odoo-cli" fi if [[ "$container_tag" =~ ^odoo: ]]; then generate-ssh-keys export GIT_SSH_PRIVATE_KEY export GIT_SSH_PUBLIC_KEY export PATH=./images/odoo/bin/:$PATH export LOCAL_PATH="./tmp" mkdir -p "$LOCAL_PATH" echo -e "\033[38;5;214mTEST\033[0m: Run git clone addons" local server_tools_git_ref=$(curl -s "https://api.github.com/repos/OCA/server-tools/branches/$ODOO_VERSION" | grep '"sha":' | head -n 1 | awk -F '"' '{print $4}') export ADDONS_GIT_REPOS="https://github.com/OCA/server-tools.git#${ODOO_VERSION}#${server_tools_git_ref}" # This will remove all SSH keys at the end ./images/odoo/bin/clone-git-addons rm -rf "$LOCAL_PATH/github.com" # Cleanup for next run echo -e "\033[38;5;214mTEST\033[0m: Install python package." export PYTHON_INSTALL="prometheus-client,pyjwt" ./images/odoo/bin/install-python-packages echo -e "\033[38;5;214mTEST\033[0m: Sync python project." cat > tmp/pyproject.toml << 'EOF' [project] name = "odoo-build-project" version = "0.1.0" description = "Test project for uv sync." dependencies = [ "requests>=2.28.0", "click>=8.0.0", ] [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [tool.uv] dev-dependencies = [ "pytest>=7.0.0", "black>=23.0.0", ] EOF ./images/odoo/bin/sync-python-project uv pip list echo -e "\033[38;5;214mTEST\033[0m: Parse url." git_proto=$(./images/odoo/bin/parse-url "git@github.com:OCA/server-tools.git" proto) [[ "$git_proto" == "ssh://" ]] && log-success "Test success." fi if [[ "$container_tag" =~ ^odoo-upgrade ]]; then echo -e "\033[38;5;214mTEST\033[0m: Show help of odoo-upgrade." docker run -it mintsystem/odoo-upgrade --help fi if [[ "$container_tag" =~ ^odoo-cli ]]; then echo -e "\033[38;5;214mTEST\033[0m: Show help of odoocli." docker_odoocli() { docker run -it \ -e ODOO_URL="$ODOO_URL" -e DB_NAME="$DB_NAME" -e ODOO_USERNAME="$ODOO_USERNAME" -e ODOO_PASSWORD="$ODOO_PASSWORD" \ mintsystem/odoocli "$@" } docker_odoocli --help fi } upload-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi # Check if path is root folder local modules="$2" if [[ ! -f "$2/__manifest__.py" ]]; then echo 'Identified as root folder' modules="$modules/*" fi for module in $modules; do if [[ -f "$module/__manifest__.py" ]]; then # Create module zip zip-module "$module" # Get path to zip file and upload url local version=$(get-module-version "$module") local module_name=$(basename "$module") local file_path="tmp/$module_name-$version.zip" load-dotenv "$1" local nextcloud_upload_url="$NEXTCLOUD_URL/remote.php/dav/files/$NEXTCLOUD_USERNAME/Odoo-Apps/" # Upload zip file curl -u "$NEXTCLOUD_USERNAME:$NEXTCLOUD_PASSWORD" -T "$file_path" "$nextcloud_upload_url" echo "File $file_path uploaded to $nextcloud_upload_url." UPLOADS+=" - name: $module_name\n" UPLOADS+=" version: $version\n" fi done echo 'odoo_apps:' echo -e "$UPLOADS" } logs() { local container="$1" container=${container:="odoo"} $CONTAINER_ENGINE logs --follow "$container" } # Install commands install() { init-venv activate-venv if [[ ! -d "$HOME/taskfile.build" ]]; then echo -e '\033[38;5;214mGit\033[0m: Clone taskfile repo' git clone https://git.taskfile.build "$HOME/taskfile.build" else echo -e '\033[38;5;214mGit\033[0m: Pull taskfile repo' git -C "$HOME/taskfile.build" pull fi # OS specific installations if [[ "$OS_RELEASE" =~ ^(Debian|Debian GNU/Linux|Ubuntu|Pop!_OS)$ ]]; then echo -e "\033[38;5;214mApt\033[0m: Update apt repo" sudo apt update echo -e "\033[38;5;214mApt\033[0m: Install python-ldap and psycopg2 build dependencies" sudo apt-get install -y libsasl2-dev libldap2-dev libssl-dev libpq-dev python3-dev echo -e "\033[38;5;214mApt\033[0m: Install xmllint, ripgrep and gettext" sudo apt install -y libxml2-utils ripgrep gettext elif [[ "$OS_RELEASE" =~ ^(Fedora Linux)$ ]]; then echo -e "\033[38;5;214mDnf\033[0m: Update dnf repo" sudo dnf update echo -e "\033[38;5;214mDnf\033[0m: Install psycopg2 build dependencies" sudo dnf install -y postgresql-devel echo -e "\033[38;5;214mApt\033[0m: Install ripgrep and gettext" sudo dnf install -y ripgrep gettext elif [[ "$OS_RELEASE" =~ ^(Darwin)$ ]]; then echo 'Welcome daring Mac user. Let'\''s try our best to get this working.' echo -e "\033[38;5;214mBrew\033[0m: Install required packages with brew." brew install gettext libsasl2 openldap openssl libxml2 grep ripgrep elif [[ "$OS_RELEASE" =~ ^(Arch Linux|SteamOS)$ ]]; then echo 'Arch is the best' echo -e "\033[38;5;214mPacman\033[0m: Install python-ldap and psycopg2 build dependencies" sudo pacman -S --noconfirm base-devel postgresql-devel echo -e "\033[38;5;214mApt\033[0m: Install ripgrep" sudo pacman -S --noconfirm ripgrep else echo "The operating system $OS_RELEASE is not supported." exit 1 fi # Install wkhtmltopdf if [[ "$OS_RELEASE" =~ ^(Arch Linux|Darwin|Debian|Debian GNU/Linux|Fedora Linux|Ubuntu|Pop!_OS|SteamOS)$ ]]; then # Source: https://gist.github.com/faniska/37f896d5e9de5fee925925d7caf3cb9e local installed_version=$(wkhtmltopdf --version 2>&1 | sed -E 's/^wkhtmltopdf +([0-9.]+).*/\1/') if [[ "$installed_version" != "0.12.4" ]]; then echo -e "\033[38;5;214mCurl\033[0m: Install wkhtmltopdf" curl -L -O https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.4/wkhtmltox-0.12.4_linux-generic-amd64.tar.xz tar xvf wkhtmltox-0.12.4_linux-generic-amd64.tar.xz sudo mv wkhtmltox/bin/wkhtmlto* /usr/bin/ rm -rf wkhtmltox wkhtmltox-0.12.4_linux-generic-amd64.tar.xz else echo 'wkhtmltopdf version is already 0.12.4' fi fi # Odoo Build requirements echo -e "\033[38;5;214mPython\033[0m: Install Odoo Build development tools" uv pip install -r requirements.txt # Odoo requirements echo -e "\033[38;5;214mPython\033[0m: Install Odoo and dependencies." cd odoo uv pip install -e . grep -v "python-ldap" requirements.txt | uv pip install -r - cd .. # Install inotify echo -e "\033[38;5;214mPython\033[0m: Install $PYTHON_NOTIFY." if [[ -n "$PYTHON_NOTIFY" ]]; then uv pip install "$PYTHON_NOTIFY" fi if [[ "$PYTHON_NOTIFY" == "inotify" ]] && ! sudo grep -q "fs.inotify.max_user_watches=524288" /etc/sysctl.d/99-sysctl.conf; then sudo bash -c 'echo "fs.inotify.max_user_watches=524288" >> /etc/sysctl.d/99-sysctl.conf' sudo sysctl -p /etc/sysctl.d/99-sysctl.conf echo -e "\033[38;5;214mSystem\033[0m: Updated fs.inotify.max_user_watches to 524288" fi # Fixes for Odoo versions if [[ "$ODOO_VERSION" == "14.0" ]] ; then echo -e "\033[38;5;214mPython\033[0m: Fix shebang for odoo binary." sed -i '1s|.*|#!'"$PWD"'/.venv14.0/bin/python|' .venv14.0/bin/odoo fi } # Process commands start() { set-addons-path if [[ "$POSTGRES_SSL" == "on" ]]; then generate-pg-ssl-keys fi ODOO_COMMAND="odoo --dev=all" if [[ "$1" =~ "odoo-nginx" ]]; then ODOO_COMMAND="odoo-nginx --dev=all" fi export ODOO_COMMAND if [[ "$1" =~ "odoo" && -a ".env.odoo" ]]; then source .env.odoo fi template-compose # Use default database if second param is not given local database="$2" if [[ -z "$2" ]]; then database="$DB_NAME" fi if [[ -z "$1" ]]; then echo "Start all container ${CONTAINER_REGISTRY}/${CONTAINER_TAG}" echo 'Open http://localhost:8069 url in your browser.' $COMPOSE_COMMAND up -d fi if [[ "$1" = "list" ]]; then echo "Options:" echo "admin" echo "db" echo "mailgate" echo "mailpit" echo "source" echo "hatch" echo "odoo" echo "odoo-nginx" fi if [[ "$1" =~ "admin" ]]; then echo 'Open http://localhost:8000 url in your browser.' open-url-with-delay "http://localhost:8000" 2 & $COMPOSE_COMMAND up -d admin fi if [[ "$1" =~ "db" ]]; then $COMPOSE_COMMAND up -d db fi if [[ "$1" =~ "mailgate" ]]; then echo 'Send emails to: smtp://localhost:587' $COMPOSE_COMMAND up -d mailgate fi if [[ "$1" =~ "mailpit" ]]; then echo 'Open http://localhost:8025 url in your browser.' open-url-with-delay "http://localhost:8025" 2 & $COMPOSE_COMMAND up -d mailpit fi if [[ "$1" =~ "source" ]]; then template-odoo-rc local port=8069 while [[ $port -le 8079 ]]; do if ! lsof -i:$port >/dev/null 2>&1; then break fi port=$((port + 1)) done echo "Open http://localhost:$port url in your browser." open-url-with-delay "http://localhost:$port/web?debug=1&db=$database" 2 & odoo --database="$database" --config "$ODOO_RC" \ --http-port="$port" --addons-path="$ADDONS_PATH" --dev=all --log-level="$LOG_LEVEL" fi if [[ "$1" =~ "hatch" ]]; then template-odoo-rc local port=8069 while [[ $port -le 8079 ]]; do if ! lsof -i:$port >/dev/null 2>&1; then break fi port=$((port + 1)) done echo "Open http://localhost:$port url in your browser." open-url-with-delay "http://localhost:$port/web?debug=1&db=$database" 2 & odoo --database="$database" --config "$ODOO_RC" \ --http-port="$port" --dev=all --log-level="$LOG_LEVEL" fi if [[ "$1" =~ "odoo" ]]; then echo "Start Odoo container ${CONTAINER_REGISTRY}/${CONTAINER_TAG}" echo 'Open http://localhost:8069 url in your browser.' open-url-with-delay "http://localhost:8069" 2 & $COMPOSE_COMMAND up -d odoo fi if [[ "$1" =~ "odoo-nginx" ]]; then echo "Start Odoo container ${CONTAINER_REGISTRY}/${CONTAINER_TAG} with nginx proxy." echo 'Open http://localhost:8080 url in your browser.' open-url-with-delay "http://localhost:8080" 2 & $COMPOSE_COMMAND up -d odoo fi } debug() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi set-addons-path local port=8069 if [[ "$1" =~ "source" ]]; then # Disable file validation to avoid frozen modules warning export PYDEVD_DISABLE_FILE_VALIDATION=1 echo 'Starting Odoo with the debugger on port 5678.' echo "Attach a debugpy debugger and open http://localhost:$port url in your browser." python -m debugpy --listen 5678 --wait-for-client $(which odoo) --database="$DATABASE" --config "$ODOO_RC" \ --http-port="$port" --addons-path="$ADDONS_PATH" --dev=all --log-level="$LOG_LEVEL" fi } run() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi template-compose echo "Run '$1' container with command '$2'." $COMPOSE_COMMAND run --rm "$1" "$2" "$3" } exec() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local container="$1" shift local cmd="$@" echo "Execute command '$cmd' with $CONTAINER_EXEC_PARAM in container '$container'." $CONTAINER_ENGINE exec $CONTAINER_EXEC_PARAM "$container" $cmd } record-with-py-spy() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local pid="$1" local output_file="tmp/speedscope-capture.json" local speedscope_url="https://www.speedscope.app/" mkdir -p tmp echo "source task source; py-spy record --pid $pid --output $output_file --format speedscope" | sudo bash -- open-with-os "$speedscope_url" } record-with-memray() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi set-addons-path local port=8069 local output_file="tmp/memray-capture.bin" if [[ "$1" =~ "source" ]]; then echo 'Starting Odoo with memray.' echo "Open http://localhost:$port url in your browser." mkdir -p tmp rm "$output_file" python3 -m memray run -o "$output_file" $(which odoo) --database="$DATABASE" --config "$ODOO_RC" \ --http-port="$port" --addons-path="$ADDONS_PATH" --dev=all --log-level="$LOG_LEVEL" python3 -m memray flamegraph "$output_file" open-with-os "tmp/memray-flamegraph-output.html" fi } remove() { template-compose if [[ -z "$1" ]]; then $COMPOSE_COMMAND down -v fi if [[ "$1" =~ "db" ]]; then $COMPOSE_COMMAND rm -f -s -v db fi if [[ "$1" =~ "admin" ]]; then $COMPOSE_COMMAND rm -f -s -v admin fi if [[ "$1" =~ "odoo" ]]; then $COMPOSE_COMMAND rm -f -s -v odoo fi if [[ "$1" =~ "mailpit" ]]; then $COMPOSE_COMMAND rm -f -s -v mail fi if [[ "$1" =~ "mailgate" ]]; then $COMPOSE_COMMAND rm -f -s -v mailgate fi } restart() { template-compose if [[ -z "$1" ]]; then $COMPOSE_COMMAND restart fi if [[ "db|mail|odoo" =~ $1 ]]; then $COMPOSE_COMMAND restart "$1" fi } stop() { template-compose if [[ -z "$1" ]]; then $COMPOSE_COMMAND stop fi $COMPOSE_COMMAND stop "$1" } generate-ssh-keys() { if [[ -z "$GIT_SSH_PRIVATE_KEY" ]]; then echo 'Generate SSH private and public keys.' mkdir -p tmp if [[ ! -f "tmp/ed25519" ]]; then ssh-keygen -t ed25519 -C admin@example.com -f tmp/ed25519 -N '' fi GIT_SSH_PRIVATE_KEY=$(cat tmp/ed25519 | base64 -w0) GIT_SSH_PUBLIC_KEY=$(cat tmp/ed25519.pub) fi } generate-pg-ssl-keys() { local keymaterial_path="tmp/postgres-odoo" local postgres_uid=70 local postgres_gid=70 if [[ "$POSTGRES_IMAGE" == "pgvector/pgvector"* ]]; then postgres_uid=999 postgres_gid=999 fi if [[ -z "$PG_SSL_CA_CRT" ]] || [[ -z "$PG_SSL_SERVER_CRT" ]] || [[ -z "$PG_SSL_SERVER_KEY" ]]; then echo 'Generate PostgreSQL SSL/TLS certificates.' mkdir -p "$keymaterial_path" if [[ ! -f "$keymaterial_path/ca.crt" ]]; then openssl genrsa -out "$keymaterial_path/ca.key" 4096 chmod 600 "$keymaterial_path/ca.key" openssl req -x509 -new -nodes -key "$keymaterial_path/ca.key" -sha256 -days 3650 \ -out "$keymaterial_path/ca.crt" -subj "/C=CH/ST=State/L=Locality/O=Organization/CN=Root CA" fi if [[ ! -f "$keymaterial_path/server.crt" ]]; then openssl genrsa -out "$keymaterial_path/server.key" 2048 openssl req -new -key "$keymaterial_path/server.key" -out "$keymaterial_path/server.csr" \ -subj "/C=CH/ST=State/L=Locality/O=Organization/CN=localhost" openssl x509 -req -in "$keymaterial_path/server.csr" -CA "$keymaterial_path/ca.crt" \ -CAkey "$keymaterial_path/ca.key" -CAcreateserial -out "$keymaterial_path/server.crt" \ -days 365 -sha256 fi local current_uid=$(stat -c '%u' "$keymaterial_path/server.key" 2>/dev/null || stat -f '%u' "$keymaterial_path/server.key" 2>/dev/null) if [[ "$current_uid" != "$postgres_uid" ]]; then chmod 600 "$keymaterial_path/server.key" chmod 644 "$keymaterial_path/server.crt" chmod 644 "$keymaterial_path/ca.crt" sudo chown "$postgres_uid:$postgres_gid" "$keymaterial_path/server.key" "$keymaterial_path/server.crt" "$keymaterial_path/ca.crt" fi fi } test-project() { # This starts the Docker environment and uses internal and external tools to run commands. if [[ "$1" == "cleanup" ]]; then echo -e "\033[38;5;214mTEST\033[0m: Cleanup test environment." drop-db test remove odoo git -C oca/partner-contact clean -df git -C oca/partner-contact checkout . remove-dotenv test else version info set -e echo -e "\033[38;5;214mTEST\033[0m: Clone module repo with https" (git clone -b "$ODOO_VERSION" https://github.com/OCA/partner-contact.git oca/partner-contact) || true echo -e "\033[38;5;214mTEST\033[0m: Start container environment." export BROWSER_OPEN=false start db,mail echo -e "\033[38;5;214mTEST\033[0m: Clone Odoo addons." generate-ssh-keys export GIT_SSH_PRIVATE_KEY export GIT_SSH_PUBLIC_KEY export ADDONS_GIT_REPOS="https://github.com/OCA/server-env.git#${ODOO_VERSION},https://github.com/OCA/server-tools.git#${ODOO_VERSION},https://github.com/Mint-System/Odoo-Apps-Server-Tools.git#${ODOO_VERSION}" run odoo clone-git-addons echo -e "\033[38;5;214mTEST\033[0m: Init Odoo database." export ODOO_ADDONS_PATH="/mnt/oca/partner-contact" export DB_NAME=test export WITHOUT_DEMO=False export ODOO_INIT_LOGIN=test export ODOO_INIT_PASSWORD=test export ODOO_INIT_LANG=de_CH export ODOO_INIT_ADDONS=server_environment_ir_config_parameter,module_auto_update,prometheus_exporter,mail_server_filter,contacts,web_enterprise export PYTHON_INSTALL=prometheus-client,manifestoo run odoo init-db echo -e "\033[38;5;214mTEST\033[0m: Start Odoo container." export ODOO_MAIL_SMTP_HOST=mail export ODOO_MAIL_SMTP_PORT=1025 export ODOO_MAIL_SMTP_ENCRYPTION=none export SERVER_WIDE_MODULES=module_change_auto_install export MODULE_AUTO_INSTALL_DISABLED=odoo_test_xmlrunner export AUTO_UPDATE_TRANSLATIONS=True export AUTO_UPDATE_MODULES_LIST=True export MAIL_DEFAULT_FROM="test" export BROWSER_OPEN=false export TEST_ADDONS_DIR=/mnt/oca/partner-contact export TEST_INCLUDE=partner_firstname start odoo TIMEOUT=60 ELAPSED=0 until curl --silent --fail "http://localhost:8069/web/login" &> /dev/null; do if [ "$ELAPSED" -ge "$TIMEOUT" ]; then echo "Odoo did not start within $TIMEOUT seconds." docker logs odoo exit 1 fi echo 'Waiting for Odoo to be ready...' sleep 3 ELAPSED=$((ELAPSED + 3)) done echo -e "\033[38;5;214mTEST\033[0m: Setup requirements.txt file." exec odoo bash -c "echo 'click>=8.0.0' > /var/lib/odoo/requirements.txt" exec odoo install-python-packages click_path=$(exec odoo bash -c "python3 -c 'import click; print(click.__file__)'") if echo "$click_path" | grep -q "/var/lib/odoo/venv/"; then log-success "click is installed in persisted venv." else log-error "click is NOT in /var/lib/odoo/venv/ — found at: $click_path" exit 1 fi echo -e "\033[38;5;214mTEST\033[0m: Init repos.yml file." cat << EOF > tmp/repos.yml github.com/Mint-System/Odoo-Apps-Partner-Contact: remotes: mint_system: https://github.com/Mint-System/Odoo-Apps-Partner-Contact.git merges: - mint_system ${ODOO_VERSION} target: mint_system ${ODOO_VERSION} EOF docker cp tmp/repos.yml odoo:/var/lib/odoo/git/repos.yml exec odoo aggregate-git-repos echo -e "\033[38;5;214mTEST\033[0m: Init OCA module." exec odoo init-module partner_firstname echo -e "\033[38;5;214mTEST\033[0m: Setup and check RPC credentials." export ODOO_URL="http://localhost:8069" export ODOO_DATABASE=test export ODOO_USERNAME=test export ODOO_PASSWORD=test if ! show-dotenv test; then create-dotenv test odoo fi show-dotenv test test-xmlrpc test echo -e "\033[38;5;214mTEST\033[0m: Install, update, disable and remove snippet." install-snippet test snippets/mint_system.base.view_partner_form.show_type.xml update-snippet test snippets/mint_system.base.view_partner_form.show_type.xml disable-snippet test snippets/mint_system.base.view_partner_form.show_type.xml remove-snippet test snippets/mint_system.base.view_partner_form.show_type.xml echo -e "\033[38;5;214mTEST\033[0m: Check module auto update." sed -i 's/First name/Firstname/g' oca/partner-contact/partner_firstname/models/res_partner.py exec odoo update-modules #FIXME: Check if module is auto updated # docker logs odoo 2>&1 | grep "Run click-odoo-update." echo -e "\033[38;5;214mTEST\033[0m: Check translations auto update." sed -i 's/Nachname/Familienname/g' oca/partner-contact/partner_firstname/i18n/de.po restart odoo #FIXME: Check if translations updated # docker logs odoo 2>&1 | grep "Updating translations for all active languages and installed modules." echo -e "\033[38;5;214mTEST\033[0m: Drop the test database." drop-db test #FIXME: These tests do not work in GitHub Runner env. # echo -e "\033[38;5;214mTEST\033[0m: Prepare container for testing." # exec odoo setup-tests # exec odoo ls /var/lib/odoo/git/github.com/Mint-System/Odoo-Apps-Partner-Contact # echo -e "\033[38;5;214mTEST\033[0m: Run module tests." # CONTAINER_EXEC_PARAM="-w /mnt/oca/partner-contact" # exec odoo run-tests echo -e "\033[38;5;214mTEST\033[0m: Run module migration." echo "Setup pre migration script for partner_firstname..." mkdir -p oca/partner-contact/partner_firstname/migrations/${ODOO_VERSION}.2.0.0 cat << EOF > oca/partner-contact/partner_firstname/migrations/${ODOO_VERSION}.2.0.0/pre-migrate.py import logging from odoo.upgrade import util _logger = logging.getLogger(__name__) def migrate(cr, version): env = util.env(cr) partners = env["res.partner"].search([]) for partner in partners: partner.name += "!" _logger.info("Updated %s partners", len(partners)) EOF sed -i "/\"version\":/c\ \"version\": \"${ODOO_VERSION}.2.0.0\"," oca/partner-contact/partner_firstname/__manifest__.py echo "Execute the module upgrade." exec odoo update-module partner_firstname #FIXME: Check if module is migrated # docker logs odoo 2>&1 | grep "module partner_firstname: Running migration" fi } # Interactive Shell commands shell() { local database="$1" if [[ -z "$database" ]]; then database="$DB_NAME" fi local code="$2" set-addons-path if [[ -n "$code" ]]; then echo "$code" | odoo shell --database "$database" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --no-http else odoo shell --database "$database" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --no-http fi } psql() { local database="$1" if [[ -z "$database" ]]; then database="$DB_NAME" fi $CONTAINER_ENGINE exec -it db psql "postgres://odoo:odoo@localhost:5432/$database" } # Database commands init-db() { local odoo_init_addons="${ODOO_INIT_ADDONS:-"web"}" local database="$1" if [[ -z "$database" ]]; then database="$DB_NAME" fi set-addons-path template-odoo-rc echo "Initialize database $database with $odoo_init_addons." if [[ -n "$ODOO_PARAM" ]]; then odoo -d "$database" -i "$odoo_init_addons" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http --load-language "$ODOO_LANGUAGE" "$ODOO_PARAM" else odoo -d "$database" -i "$odoo_init_addons" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http --load-language "$ODOO_LANGUAGE" fi } import-csv() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local database="$1" local file_path="$2" # Use default database if second param is not given if [[ -z "$2" ]]; then database="$DB_NAME" file_path="$1" fi local file_name=$(basename "$file_path") echo "Importing $file_name to ${database}..." $CONTAINER_ENGINE cp "$file_path" db:/tmp/ $CONTAINER_ENGINE exec db psql "postgres://odoo:odoo@localhost:5432/$database" -c "\copy $file_name FROM '/tmp/$file_name' DELIMITER ',' CSV HEADER;" } drop-db() { local database="$1" if [[ -z "$database" ]]; then database="$DB_NAME" fi $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost:5432/postgres" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$database';" $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost:5432/postgres" -c "DROP DATABASE \"$database\";" } load-language() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local database="$1" local language="$2" # Use default database if second param is not given if [[ -z "$2" ]]; then database="$DB_NAME" language="$1" fi set-addons-path echo "Setup language $language for database $database:" odoo -d "$database" -u all --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http --load-language "$language" } set-admin() { local database="$1" if [[ -z "$database" ]]; then database="$DB_NAME" fi $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost:5432/$database" -c "UPDATE res_users SET active=true,password='\$pbkdf2-sha512\$25000\$JuScEwIg5JzTGqNUivFeqw\$yWTOcix2Afr3XGP2NPY7w4w49e9vpsu14NRndDYXAkbtMF4zkrmx6inVsoLl0zZY30xI/0GzhwonWsK9TUmjWA' WHERE id=2;" $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost:5432/$database" -c "SELECT login, password FROM res_users WHERE id=2;" } clear-assets() { local database="$1" if [[ -z "$database" ]]; then database="$DB_NAME" fi echo "Delete these assets for $database:" $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost/$database" -c "select id,name from ir_attachment where res_model='ir.ui.view' and name like '%assets_%';" $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost/$database" -c "delete from ir_attachment where res_model='ir.ui.view' and name like '%assets_%';" } clear-views() { local database="$1" if [[ -z "$database" ]]; then database="$DB_NAME" fi echo "Archive views with prefix '$SNIPPET_PREFIX." $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost/$database" -c "SELECT id,name from ir_ui_view WHERE name like '%$SNIPPET_PREFIX%';" $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost/$database" -c "UPDATE ir_ui_view SET active=false WHERE name like '%$SNIPPET_PREFIX%';" echo 'Archive views with prefix '\''Odoo Studio:'\''.' $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost/$database" -c "SELECT id,name from ir_ui_view WHERE name like '%Odoo Studio:%';" $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost/$database" -c "UPDATE ir_ui_view SET active=false WHERE name like '%Odoo Studio:%';" echo "Delete all active views for $database in $CONTAINER :" $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost/$database" -c "DELETE FROM report_layout WHERE view_id IN (SELECT id FROM ir_ui_view WHERE active = TRUE);" $CONTAINER_ENGINE exec -i db psql "postgres://odoo:odoo@localhost/$database" -c "WITH RECURSIVE views_to_delete AS (SELECT id, inherit_id FROM ir_ui_view WHERE active = TRUE AND inherit_id IS NULL UNION ALL SELECT v.id, v.inherit_id FROM ir_ui_view v JOIN views_to_delete p ON v.inherit_id = p.id) DELETE FROM ir_ui_view WHERE id IN (SELECT id FROM views_to_delete);" } clear-filestore() { echo "Remove directory $HOME/.local/share/Odoo/filestore/$1" rm -rf "$HOME/.local/share/Odoo/filestore/$1" } disable-mailserver() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi load-dotenv "$1" images/odoo-cli/bin/odoocli --method 'write' --model 'ir.mail_server' --field 'active' --value False --domain "[]" images/odoo-cli/bin/odoocli --method 'write' --model 'fetchmail.server' --field 'active' --value False --domain "[]" } change-uuid() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi load-dotenv "$1" local uuid=$(uuidgen) echo "Set uuid $uuid for $1." images/odoo-cli/bin/odoocli --method 'write' --model 'ir.config_parameter' --domain "[('key', '=', 'database.uuid')]" --field 'value' --value "'$uuid'" } update-module-list() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi set-addons-path echo "env['ir.module.module'].update_list()" | odoo shell --database "$1" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --no-http } patch-database() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local database="$1" local filename="$2" # Use default database if second param is not given if [[ -z "$2" ]]; then database="$DB_NAME" filename="$1" fi echo "Apply patch $filename to Database $database." cat "$filename" | $CONTAINER_ENGINE exec -i db psql "postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost/$database" echo 'Patching database succeeded' } # Report commands cloc-odoo() { local database="$1" if [[ -z "$database" ]]; then database="$DB_NAME" fi set-addons-path odoo cloc -d "$database" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http } visualize-dependencies() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi mkdir -p tmp ./bin/visualize-dependencies "$1" open-with-os tmp/odoo-module-dependencies.html cat tmp/odoo-module-dependencies.mmd } # Setup commands install-requirements() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local database="$1" local file_path="$2" # Use default database if second param is not given if [[ -z "$2" ]]; then database="$DB_NAME" file_path="$1" fi local file_name=$(basename "$file_path") if ! [[ "$file_name" =~ .*requirements\.txt$ ]]; then echo '$1 must point to requirements.txt file.' exit fi local container_running=$($CONTAINER_ENGINE container inspect -f '{{.State.Running}}' odoo) if [[ "$container_running" == "true" ]]; then echo 'Installing python packages in container...' $CONTAINER_ENGINE exec -u root odoo python -m pip install -r "/mnt/$file_path" else echo 'Installing python packages in virutal env...' uv pip install -r "$file_path" fi } list-packages() { uv pip list } # Module commands init-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local database="$1" local module_path="$2" # Use default database if second param is not given if [[ -z "$2" ]]; then database="$DB_NAME" module_path="$1" fi # Check if module path is a module repo if [[ -f "$module_path/README.md" ]]; then # Look for manifest files in repo folder # Get parent folder name of manifest files # Create a comma separated string of folder names local modules=$(get-modules "$module_path" basename) else local modules=$(basename "$module_path") fi set-addons-path echo "Initialize module $modules on ${database}..." odoo -d "$database" -i "$modules" --config "$ODOO_RC" \ --addons-path="$ADDONS_PATH" --stop-after-init --no-http \ --log-level="$LOG_LEVEL" } update-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local database="$1" local module_name=$(basename "$2") # Use default database if second param is not given if [[ -z "$2" ]]; then database="$DB_NAME" module_name=$(basename "$1") fi set-addons-path echo "Updating module $module_name on ${database}..." if [[ -n "$ODOO_PARAM" ]]; then odoo --database "$database" --update "$module_name" \ --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http "$ODOO_PARAM" else odoo --database "$database" --update "$module_name" \ --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http fi } translate-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module_path="$1" local module_name=$(basename "$1") local language="$2" local database="$3" if [[ -z "$2" ]]; then language="$ODOO_LANGUAGE" fi if [[ -z "$3" ]]; then database="$DB_NAME" fi set-addons-path echo "Generate $language tranlsation file for $module_name on ${database}..." mkdir -p "$module_path/i18n/" odoo -d "$database" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --modules "$module_name" -l "$language" --i18n-export "$module_path/i18n/$language.po" # If lang is de_CH then rename the file to de.po. if [[ "$language" == "de_CH" ]]; then mv "$module_path/i18n/$language.po" "$module_path/i18n/de.po" fi } test-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module=$(basename "$1") set-addons-path echo "Testing module $module..." odoo -d "$DB_NAME" -i "$module" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http --test-enable } test-modules() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local modules=$(get-modules "$1" basename) set-addons-path echo "Collect requirements.txt files in $1." find "$1" -type f -name "requirements.txt" -exec uv pip install -r {} \; echo "Collect test-requirements.txt files in $1." find "$1" -type f -name "test-requirements.txt" -exec uv pip install -r {} \; depends_modules=$(manifestoo --select-addons-dir "$1" list-depends --separator=,) echo "Setup test database with "$ODOO_LANGUAGE" lang and depends modules: $depends_modules" odoo -d "$DB_NAME" -i "$depends_modules" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http --load-language "$ODOO_LANGUAGE" echo "Testing modules: $modules" odoo -d "$DB_NAME" -i "$modules" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http --test-enable } pytest-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module_path="$1" echo "Setup custom tmp/odoo.conf for pytest." set-addons-path local odoo_config="tmp/odoo.conf" echo "[options]" > "$odoo_config" echo "addons_path=$ADDONS_PATH" >> "$odoo_config" echo "db_host = localhost" >> "$odoo_config" echo "db_password = odoo" >> "$odoo_config" echo "db_user = odoo" >> "$odoo_config" echo "Run pytest with odoo-http option..." pytest -s --odoo-http --odoo-config="$odoo_config" --odoo-database="$DB_NAME" "$module_path" } remove-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local database="$1" local module_name=$(basename "$2") # Use default database if second param is not given if [[ -z "$2" ]]; then database="$DB_NAME" module_name=$(basename "$1") fi set-addons-path echo "Remove module $module_name" echo "self.env['ir.module.module'].search([('name', '=', '$module_name')]).button_immediate_uninstall()" | odoo shell --database "$database" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --no-http } create-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module_path="$1" local module_name=$(basename "$module_path") local parent_dir=$(dirname "$module_path") echo "Scaffolding module $module_name in ${parent_dir}..." odoo scaffold "$module_name" "$parent_dir" -t templates/module echo 'Prompt for module manifest values...' export MODULE_TITLE=$(echo "$module_name" | sed 's/[^_]\+/\L\u&/g' | sed 's/_/ /g') read -p "Summary: " MODULE_SUMMARY export MODULE_SUMMARY while true; do read -p "License (AGPL-3/OPL-1): " MODULE_LICENSE [[ "$MODULE_LICENSE" == "AGPL-3" || "$MODULE_LICENSE" == "OPL-1" ]] && break echo "Invalid. Use AGPL-3 or OPL-1." done export MODULE_LICENSE read -p "Depends (comma-separated and quoted): " MODULE_DEPENDS export MODULE_DEPENDS while true; do read -p "Demo? (y/n): " DEMO_RESPONSE if [[ "$DEMO_RESPONSE" == "y" ]]; then export MODULE_DEMO_KEY='"demo": ["demo/demo.xml"],' mkdir -p "$module_path/demo" cp templates/demo.xml "$module_path/demo/demo.xml" break elif [[ "$DEMO_RESPONSE" == "n" ]]; then export MODULE_DEMO_KEY="" break else echo "Answer y or n." fi done while true; do read -p "Test instructions? (y/n): " TEST_RESPONSE if [[ "$TEST_RESPONSE" == "y" ]]; then mkdir -p "$module_path/tests" touch "$module_path/tests/TEST_INSTRUCTIONS.rst" break elif [[ "$TEST_RESPONSE" == "n" ]]; then break else echo "Answer y or n." fi done while true; do read -p "Security? (y/n): " TEST_RESPONSE if [[ "$TEST_RESPONSE" == "y" ]]; then export MODULE_DATA_KEY='"data": ["security/security.xml"],' mkdir -p "$module_path/security" cp templates/security.xml "$module_path/security/security.xml" break elif [[ "$TEST_RESPONSE" == "n" ]]; then export MODULE_DATA_KEY='"data": [],' break else echo "Answer y or n." fi done export ODOO_VERSION envsubst < templates/module/__manifest__.py > "$module_path/__manifest__.py" echo "Templated the mdoule $module_path" } generate-module-docs() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module_path="$1" local manifest_path="$module_path/__manifest__.py" echo 'Setup readme folder' mkdir -p "$module_path/readme" cp -n -r "templates/module/readme" "$module_path" echo 'Update DESCRIPTION.rst' export MODULE_SUMMARY=$(python3 -c "import ast; d=ast.literal_eval(open('$manifest_path').read()); print(d.get('summary', '').replace('\n', ' ').strip())") envsubst < templates/module/readme/DESCRIPTION.md > "$module_path/readme/DESCRIPTION.md" echo 'Update CONTRIBUTORS.rst' export GIT_USER_NAME="$(git config user.name)" export GIT_USER_EMAIL="$(git config user.email)" envsubst < templates/module/readme/CONTRIBUTORS.md > "$module_path/readme/CONTRIBUTORS.md" echo 'Copy icon.png' cp templates/module/static/description/icon.png "$module_path/static/description/icon.png" echo 'Copy LICENSE' cp templates/module/LICENSE "$module_path/" echo 'Touch __init__.py' touch "$module_path/__init__.py" local repo_url=$(git -C "$(dirname $module_path)" remote get-url origin) local repo_name=$(basename "${repo_url%.git}") echo "Generate OCA readme for $USER_ORG/$repo_name org." oca-gen-addon-readme --org-name=$USER_ORG --repo-name=$repo_name --branch=$ODOO_VERSION --addon-dir=$module_path } generate-module-migration() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module_path="$1" local module_version=$(get-module-version $module_path) echo "Create migrations folder for version $module_version." mkdir -p "$module_path/migrations/$module_version" echo "Copy pre-migrate template." cp "templates/migrations/pre-migrate.py" "$module_path/migrations/$module_version" } generate-module-security() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi local module_name=$(basename "$1") local parent_dir=$(dirname "$1") local model_dot_name="$2" local model_name=$(echo "$model_dot_name" | tr '.' '_') local access_file="$parent_dir/$module_name/security/ir.model.access.csv" mkdir -p "$parent_dir/$module_name/security" if [[ ! -f "$access_file" ]]; then echo "Init security file: $access_file" echo "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink" > "$access_file" fi # id # name # model_id:id # group_id:id # perm_read # perm_write # perm_create # perm_unlink echo "Add model access to $access_file" echo "access_${model_name}_user,access.$model_dot_name.user,model_${model_name},base.group_user,1,0,0,0" >> "$access_file" echo "access_${model_name}_manager,access.$model_dot_name.manager,model_${model_name},base.group_erp_manager,1,1,1,1" >> "$access_file" echo "Update file $module_name/__manifest__.py with:" echo -e "\t\"security/ir.model.access.csv\"," } create-repo() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local repo_path="$1" local repo_name=$(basename "$repo_path") # If repo already exists, create new branch if [[ -d "$repo_path/.git" ]]; then echo "Create new branch $repo_name in $repo_path" git -C "$repo_path" checkout --orphan="$ODOO_VERSION" echo "Remove all files from $repo_path" rm -rf "$repo_path/*" fi } export-module-vars() { export MODULE_NAME=$(basename "$1") # partner_email_history export MODEL_DOT_NAME="$2" # res.partner export MODEL_NAME=$(echo "$MODEL_DOT_NAME" | tr '.' '_') # res_partner export NAME=$(echo "$MODEL_DOT_NAME" | cut -d"." -f2 | sed 's/[^_]\+/\L\u&/g') # Partners export MODEL_DESCRIPTION=$(echo "$MODEL_NAME" | sed 's/[^_]\+/\L\u&/g' | sed 's/_/ /g') # Res Partner export MODEL_CAMEL_NAME=$(echo "$MODEL_DESCRIPTION" | sed 's/ //g') # ResPartner } export-module-inherit-vars() { export MODEL_DOT_NAME="$3" # sale.order export VIEW_REF="$2" # sale.view_order_form export MODEL_NAME=${MODEL_DOT_NAME//\./_} # sale_order export MODULE_NAME=$(basename "$1") # sale_helm export SRC_MODULE_NAME=$(echo "$VIEW_REF" | cut -d"." -f1) # sale export XML_ID=$(echo "$VIEW_REF" | cut -d"." -f2) # view_order_form } generate-module-model() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi local module_path="$1" echo 'Substitute model template' export-module-vars "$1" "$2" envsubst < templates/model.py > "$module_path/models/$MODEL_NAME.py" echo "from . import $MODEL_NAME" >> "$module_path/models/__init__.py" echo "File "$module_path/models/$MODEL_NAME.py created. } generate-module-translation() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module_path="$1" local module_name=$(basename "$1") echo "Translate module $module_name." translate-module "$module_path" echo "Translate the de.po with llm..." task update-with-llm "$module_path/i18n/de.po" "Add missing translations." } generate-module-inherit() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi local module_path="$1" local module_name=$(basename "$1") echo 'Substitute model template' export-module-vars "$1" "$2" envsubst < templates/inherit.py > "$module_path/models/$MODEL_NAME.py" echo "from . import $MODEL_NAME" >> "$module_path/models/__init__.py" echo "File "$module_path/models/$MODEL_NAME.py created. } generate-module-views() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi local module_path="$1" local module_name=$(basename "$1") echo 'Substitute view template' mkdir -p "$module_path/views" export-module-vars "$1" "$2" envsubst < templates/model_views.xml > "$module_path/views/${MODEL_NAME}_views.xml" echo "Update file $module_name/__manifest__.py with:" echo -e "\t\"views/${MODEL_NAME}_views.xml\"," } generate-module-snippet() { if [[ -z "$1" ]]; then echo '$1/path is empty.' exit fi if [[ -z "$2" ]]; then echo '$2/ref is empty.' exit fi if [[ -z "$3" ]]; then echo '$3/model is empty.' exit fi local module_path="$1" echo 'Substitute snippet template' mkdir -p "$module_path/views" export-module-inherit-vars "$1" "$2" "$3" # addons/sale_workflow/sale_helm sale.view_order_form sale.order envsubst < templates/model_snippet.xml > "$module_path/views/${MODEL_NAME}_views.xml" echo "Update file $MODULE_NAME/__manifest__.py with:" echo -e "\t\"views/${MODEL_NAME}_views.xml\"," } generate-module-report() { if [[ -z "$1" ]]; then echo '$1/path is empty.' exit fi if [[ -z "$2" ]]; then echo '$2/model is empty.' exit fi local module_path="$1" local module_name=$(basename "$1") echo 'Substitute view template' mkdir -p "$module_path/report" export-module-vars "$1" "$2" envsubst < templates/model_templates.xml > "$module_path/report/${MODEL_NAME}_templates.xml" echo "Update file $module_name/__manifest__.py with:" echo -e "\t\"report/${MODEL_NAME}_templates.xml\"," } release-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local version=$(get-module-version "$1") cd "$1" || exit local last_tag=$(git describe --tags) local release_notes=$(git log "$last_tag"..HEAD --oneline) echo "Tag with v${version}..." git tag -a "v$version" -m "v$version" echo 'Release notes:' echo "$release_notes" } zip-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi # Get version and module name local version=$(get-module-version "$1") local module=$(basename "$1") # Copy module to tmp folder echo "Copy module $module version $version to tmp folder" local module_name="$module-$version" local module_path="tmp/$module_name" rm -rf "$module_path" mkdir -p "$module_path" cp -r "$1/." "$module_path" find "$module_path" -type d -exec chmod u=rwx,go=rx {} \; find "$module_path" -type f -exec chmod u=rw,go=r {} \; # Configure and zip module echo 'Remove auto install option' sed -i'' -e "s/'auto_install': True,/'auto_install': False,/" "$module_path/__manifest__.py" echo 'Remove pycache and hidden folders' find "$module_path" | grep -E "(\.git|__pycache__|\.pyc|\.pyo$)" | xargs rm -rf echo "Zip module $module" cd tmp || exit rm -f "$module_name".zip zip -q -r "$module_name".zip "$module_name" cd .. || exit } # Upgrade commands prepare-migration-brach() { if [[ -z "$1" ]]; then echo '$1/path is empty.' exit fi if [[ -z "$2" ]]; then echo '$2/module is empty.' exit fi if [[ -z "$3" ]]; then echo '$3/version is empty.' exit fi repo="$1" module="$2" version="$3" echo "Create migration branch for $mdoule module in $repo for version $ODOO_VERSION" git -C "$repo" checkout -b "$ODOO_VERSION-mig-$module" "origin/$ODOO_VERSION" echo "Checkout $module from $version" git -C "$repo" checkout "$version" "$module" # echo "Create patch file from version $version to $ODOO_VERSION" # git format-patch --keep-subject --stdout origin/$ODOO_VERSION..origin/$version -- $module | git am -3 --keep echo "Run pre-commit" cd "$repo" pre-commit run -a echo "Commit the changes" git add --all git commit -m "[IMP] $module: pre-commit auto fixes" --no-verify echo "Add git remote for OCA" repo_name="$repo" git remote add oca $(git remote get-url origin) echo "Add git remote for $USER_ORG" repo_name="$repo" user_org="${USER_ORG,,}" git remote add "$user_org" "https://github.com/${USER_ORG}/Odoo-Apps-$repo_name" } migrate-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module_path="$1" local source_odoo_version=$(get-module-version $module_path | cut -d'.' -f1,2) local module=$(basename "$module_path") # local addons_path=$(dirname "$module_path") # echo "Run Odoo upgrade_code command for $module_path from ${source_odoo_version}..." # odoo upgrade_code --addons-path "$addons_path" --from "$source_odoo_version" # echo "Replace manifest version $source_odoo_version with $ODOO_VERSION." # sed -i -E "s/$source_odoo_version/$ODOO_VERSION/g" "$module_path/__manifest__.py" echo "Run odoo-module-migrator for $module with source $source_odoo_version and target ${ODOO_VERSION}..." odoo-module-migrate --directory $(dirname "$module_path") --modules $(basename "$module_path") \ --init-version-name $source_odoo_version --target-version-name $ODOO_VERSION --no-commit } upgrade-odoo() { source ./bin/upgrade-odoo upgrade-odoo "$@" } # Snippet commands reset-views() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local database="$1" local key=$(basename "$2") # Use default database if second param is not given if [[ -z "$2" ]]; then database="$DB_NAME" key=$(basename "$1") fi set-addons-path echo "Reset views for key $key" echo "self.env['ir.ui.view'].search([('key', 'ilike', '$key')]).reset_arch(mode='hard')" | odoo shell --database "$database" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --no-http } create-snippet() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi export INHERIT_ID=$(echo "$1" | cut -d. -f1-2) local file="snippets/$SNIPPET_PREFIX.$1.xml" echo "Create snippet from template: $file" envsubst '$INHERIT_ID' < templates/snippet.xml > "$file" } install-snippet() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi # Generate view name from filename local file=$(basename "$2") local name="${file%.xml}" local inherit_id=$(echo "$name" | cut -d. -f 2,3 ) local module_name=$(echo "$inherit_id" | cut -d. -f 1 ) local xml_id=$(echo "$inherit_id" | cut -d. -f 2 ) local type=$(echo 'cat //data/@type' | xmllint --shell "$2" | awk -F'[="]' '!/>/{print $(NF-1)}') local model=$(echo 'cat //data/@model' | xmllint --shell "$2" | awk -F'[="]' '!/>/{print $(NF-1)}') local priority=$(echo 'cat //data/@priority' | xmllint --shell "$2" | awk -F'[="]' '!/>/{print $(NF-1)}') # Set default values : ${type:='qweb'} load-dotenv "$1" echo "Search for view with name $module_name.$xml_id" inherit_id=$(images/odoo-cli/bin/odoocli --method "read" --model "ir.model.data" --field "res_id" --domain "[('name', '=', '$xml_id'), ('module', '=', '$module_name')]") echo "Found view id $inherit_id" echo "Apply view $name" images/odoo-cli/bin/odoocli --method 'create' --model 'ir.ui.view' --value "{ 'name': '$name', 'type': '$type', 'model': '$model', 'mode': 'extension', 'priority': $priority, 'inherit_id': $inherit_id, 'arch_base': '''$(cat "$2")''' }" } update-snippet() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi # Generate view name from filename local file=$(basename "$2") local name="${file%.xml}" load-dotenv "$1" echo "Update view with name $name" images/odoo-cli/bin/odoocli --method 'write' --model 'ir.ui.view' --include-archived --domain "[('name', '=', '$name')]" --field 'arch_base' --value "'''$(cat "$2")'''" enable-snippet "$1" "$2" } disable-snippet() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi # Get name from path # path: snippets/mint_system.base.view_partner_tree.show_type.xml # name: mint_system.base.view_partner_tree.show_type local name="$2" if [[ "$2" == *.xml ]]; then local file=$(basename "$2") name="${file%.xml}" fi load-dotenv "$1" echo "Disable view with name $name" images/odoo-cli/bin/odoocli --method 'write' --model "ir.ui.view" --domain "[('name', '=', '$name')]" --field "active" --value "False" } enable-snippet() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi # Get name from path local name="$2" if [[ "$2" == *.xml ]]; then local file=$(basename "$2") name="${file%.xml}" fi load-dotenv "$1" echo "Enable view with name $name" images/odoo-cli/bin/odoocli --method 'write' --model "ir.ui.view" --include-archived --domain "[('name', '=', '$name')]" --field "active" --value "True" } remove-snippet() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi # Get name from path local name="$2" if [[ "$2" == *.xml ]]; then local file=$(basename "$2") name="${file%.xml}" fi load-dotenv "$1" echo "Remove view with name $name" images/odoo-cli/bin/odoocli --method "unlink" --model "ir.ui.view" --domain "[('name', '=', '$name')]" } lint-snippets() { echo 'Lint snippets files...' ./bin/lint-snippets } # Project commands load-version() { if [[ -n "$1" ]]; then ODOO_VERSION="$1" fi echo "Copy .gitmodules, .gitmodules.csv and .python-version from 'versions/$ODOO_VERSION'." cp "versions/$ODOO_VERSION/.gitmodules" . cp "versions/$ODOO_VERSION/.gitmodules.csv" . cp "versions/$ODOO_VERSION/.python-version" . } checkout() { if [[ -z "$1" ]]; then echo "\$1 is empty."; exit; fi ODOO_VERSION="$1" # Load .gitmodules and .python-version load-version "$ODOO_VERSION" # Set .env load-revision "$ODOO_VERSION" # Clone the addons clone-git-folder . # Checkout commit ref checkout-git-folder "$ODOO_VERSION" . # Upate branch switch-git-folder "$ODOO_VERSION" . } save-version() { local git_folders_file="versions/$ODOO_VERSION/" echo "Save git refs to .gitmodules.csv." local git_folders=$(ls-git-folder .) echo "path,rev" > ".gitmodules.csv" for git_folder in $git_folders; do local commit_ref=$(git -C "$git_folder" rev-parse HEAD) echo "$git_folder,$commit_ref" >> ".gitmodules.csv" done echo "Copy .gitmodules and .python-version to 'versions/$ODOO_VERSION'" cp ".gitmodules" "versions/$ODOO_VERSION/" cp ".gitmodules.csv" "versions/$ODOO_VERSION/" cp ".python-version" "versions/$ODOO_VERSION/" } list-versions() { ls -1 versions } check-version() { echo 'Check if Odoo version matches the release file:' local odoo_version=$(echo "$ODOO_VERSION" | cut -d'.' -f1) grep -E "version_info.+$odoo_version" odoo/odoo/release.py } # Revision commands list-revision() { ls -1 revisions } show-revision() { if [[ -n "$1" ]]; then ODOO_REVISION="$1" fi echo "Odoo Revision: $ODOO_REVISION" source "revisions/$ODOO_REVISION" echo "Odoo Ref: $ODOO_REF" echo "Odoo Enterprise Ref: $ODOO_ENTERPRISE_REF" echo "Odoo Themes Ref: $ODOO_THEME_REF" } load-revision() { if [[ -n "$1" ]]; then ODOO_REVISION="$1" fi echo 'Update revision var in .env file.' if grep -q "ODOO_REVISION" .env; then sed -i -e "s/^ODOO_REVISION=.*/ODOO_REVISION=${ODOO_REVISION}/" .env else echo "ODOO_REVISION=${ODOO_REVISION}" >> .env fi echo "Load refs for revision: $ODOO_REVISION" export $(cat "revisions/$ODOO_REVISION" | sed 's/#.*//g' | xargs) } checkout-revision() { if [[ -n "$1" ]]; then ODOO_REVISION="$1" fi load-revision "$ODOO_REVISION" echo 'Checkout Odoo git repos...' git -C odoo checkout $ODOO_REF git -C enterprise checkout $ODOO_ENTERPRISE_REF git -C themes checkout $ODOO_THEME_REF } create-revision() { if [[ -z "$1" ]]; then echo "\$1 is empty."; exit; fi local revision="$1" local odoo_date=$(echo "$revision" | cut -d'.' -f3-) local before_date=$(date -d "$odoo_date -1 day" +"%Y-%m-%d") echo "Checkout Odoo repos on $before_date." # Get git commit by date cd odoo git fetch echo -e "\nShow last 3 commits from $before_date:" git --no-pager log -n 3 --pretty=format:"%h %ad %s" --date=short --before="$before_date" origin/$ODOO_VERSION local ref=$(git rev-list -n 1 --before="$before_date" origin/$ODOO_VERSION) git checkout "$ref" cd .. cd enterprise git fetch echo -e "\nShow last 3 commits from $before_date:" git --no-pager log -n 3 --pretty=format:"%h %ad %s" --date=short --before="$before_date" origin/$ODOO_VERSION local enterprise_ref=$(git rev-list -n 1 --before="$before_date" origin/$ODOO_VERSION) git checkout "$enterprise_ref" cd .. cd themes git fetch echo -e "\nShow last 3 commits from $before_date:" git --no-pager log -n 3 --pretty=format:"%h %ad %s" --date=short --before="$before_date" origin/$ODOO_VERSION local theme_ref=$(git rev-list -n 1 --before="$before_date" origin/$ODOO_VERSION) git checkout "$theme_ref" cd .. echo -e "\nCreate revision: $revision" export ODOO_REVISION=$revision export ODOO_REF=$ref export ODOO_ENTERPRISE_REF=$enterprise_ref export ODOO_THEME_REF=$theme_ref envsubst < templates/odoo_revision > "revisions/$revision" chmod +x "revisions/$revision" echo "Checkout revision: $revision" checkout-revision "$revision" } commit-and-push-revision() { if [[ -n "$1" ]]; then ODOO_REVISION="$1" fi echo 'Remove existing tag...' git tag -d "$ODOO_REVISION" echo "Commit with revision $ODOO_REVISION tag..." git commit --all --message "feat(revision): release $ODOO_REVISION" git tag "$ODOO_REVISION" echo 'Push tags...' git push git push -f --tags } # RPC commands test-xmlrpc() { load-dotenv "$1" echo "Connect to database and count res.partner records: $(images/odoo-cli/bin/odoocli --model 'res.partner' --method 'search_count' --domain '[]')" } odoobin(){ odoo "$@" } odoocli(){ images/odoo-cli/bin/odoocli "$@" } manifestoo(){ set-addons-path ./.venv$ODOO_VERSION/bin/manifestoo --addons-path="$ADDONS_PATH" "$@" } export-website-data() { if [[ -n "$1" ]]; then load-dotenv $1 fi export ODOO_URL export DB_NAME export ODOO_USERNAME export ODOO_PASSWORD export ODOO_WEBSITE_ID echo -e "\nPython Output:\n\n---" python "bin/export-website-data" local exit_code=$? echo -e "---\n" echo -e "Exit code: $exit_code\n" } import-website-data() { if [[ -n "$1" ]]; then load-dotenv $1 fi export ODOO_URL export DB_NAME export ODOO_USERNAME export ODOO_PASSWORD export ODOO_WEBSITE_ID echo -e "\nPython Output:\n\n---" python "bin/import-website-data" local exit_code=$? echo -e "---\n" echo -e "Exit code: $exit_code\n" } # Vuepress commands install-vuepress() { echo 'Install Node.' pnpm env use --global lts echo 'Install npm packages.' pnpm install } dev-vuepress() { pnpm run dev } build-vuepress() { pnpm run build echo "Create vuepress snippets folders." mkdir -p .vuepress/dist/snippets echo "Copy snippet files to snippets folder." cp snippets/*.xml .vuepress/dist/snippets } serve-vuepress() { cd .vuepress/dist pnpx serve } if [[ "$1" == *","* ]]; then # Split comma-separated commands IFS=',' read -ra COMMANDS <<< "$1" for cmd in "${COMMANDS[@]}"; do # Trim whitespace cmd=$(echo "$cmd" | xargs) if declare -f "$cmd" > /dev/null; then activate-venv "$cmd" else echo "Unknown command: $cmd" help exit 1 fi done elif declare -f "$1" > /dev/null; then activate-venv "$1" "${@:2}" else case "$1" in *) echo "Unknown command: $1" help exit 1 ;; esac fi