#!/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 cmd_width=26 local opt_width=20 local desc_width=78 local column="| %-${cmd_width}s | %-${opt_width}s | %-${desc_width}s |\n" printf "$column" 'Command' 'Option' 'Description' echo "|$(printf '%*s' $((cmd_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((opt_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((desc_width + 2)) '' | tr ' ' '-')|" # Env printf "$column" 'backup-env-files' '[path]' 'Archive and copy env files to target location.' printf "$column" 'copy-env' '[env][env]' 'Copy env file.' printf "$column" 'create-env' '[env][type]' 'Create env file of type nextcloud, odoo, upgrade.' printf "$column" 'edit-env' '[env]' 'Open env file in default editor.' printf "$column" 'list-env' '' 'List env files.' printf "$column" 'load-dotenv' '' 'Restore content of .env from pass entry.' printf "$column" 'load-env' '[env]' 'Load and export env file.' printf "$column" 'remove-env' '[env]' 'Remove environment config.' printf "$column" 'rename-env' '[env][env]' 'Rename env file.' printf "$column" 'restore-env-files' '[path]' 'Extract and copy env files from backup file.' printf "$column" 'save-dotenv' '' 'Store content of .env in pass entry.' printf "$column" 'show-env' '[env]' 'Output content of the env file.' # Git printf "$column" 'aggregate-git-folders' '[path]' 'Run gitaggregate for the provide repos.yaml.' printf "$column" 'add-git-submodule' '[url] [path]' 'Add git submodule.' printf "$column" 'checkout' '[version]' 'Checkout Odoo version.' printf "$column" 'checkout-git-folder' '' 'Checkout git commit.' printf "$column" 'commit-git-folder' '[message][path]' 'Commit all changes in path.' printf "$column" 'clean-git-folder' '' 'Clean git folder.' printf "$column" 'clone-git-folder' '[submodule][version]' 'Clone git folder listed in the .gitmodules file.' printf "$column" 'create-git-feature-branch' '[path]' 'Create feature branch for Odoo module.' printf "$column" 'create-git-mig-branch' '[path]' 'Create migration branch for Odoo module.' printf "$column" 'download-git-folder' '[grep]' 'Download git folder listed in the .gitmodules file.' printf "$column" 'status-git-folder' '[path]' 'Show status for git folder in path.' printf "$column" 'list-git-folder' '[grep]' 'List path and url of git folders.' printf "$column" 'pull-git-folder' '' 'Pull all git folders listed in the .gitmodules file.' printf "$column" 'push-git-folder' '' 'Push all git folders in path.' printf "$column" 'remove-git-folders' '' 'Delete all git folders.' printf "$column" 'remove-git-submodule' '[path]' 'Remove a git submodule.' printf "$column" 'ls-git-folder' '[grep]' 'List git folders path space separated.' printf "$column" 'reset-git-folder' '' 'Abort rebase and reset submodules listed in the .gitmodules file.' printf "$column" 'stage-git-folder' '[path]' 'Stage all files in git folders in path.' printf "$column" 'switch-git-folder' '[version]' 'Switch branch for all git folders listed in the .gitmodules file.' printf "$column" 'sync-git-folder' '' 'Switch, stash and pull all git folders.' # Docker printf "$column" 'archive-docker-tags' '' 'Archive Docker image tags on hub older than 1 year.' printf "$column" 'build' '[folder][output]' 'Build Docker image from target folder.' printf "$column" 'login-docker' '[user][token]' 'Setup Docker Hub login credentials.' printf "$column" 'login-podman' '[user][token]' 'Login into registry with podman.' printf "$column" 'container-ps' '' 'List container processes.' printf "$column" 'push' '[folder]' 'Publish Docker image from target folder.' printf "$column" 'remove' '[name]' 'Remove containers and volumes.' printf "$column" 'test-image' '[folder]' 'Test Docker image from target folder.' printf "$column" 'test-odoo-version-regex' '' 'Test the Odoo module version regex filter.' # Vuepress printf "$column" 'build-vuepress' '' 'Create Vuepress build.' printf "$column" 'install-vuepress' '' 'Install Node build dependencies.' printf "$column" 'dev-vuepress' '' 'Start Vuepress development server.' printf "$column" 'serve-vuepress' '' 'Serve Vuepress build.' # Database printf "$column" 'change-uuid' '[env]' 'Change database uuid via xmlrpc.' printf "$column" 'clear-assets' '[db]' 'Clear all assets of Odoo database.' printf "$column" 'clear-filestore' '[db]' 'Clear local filestore folder. No param will clear all filestores.' printf "$column" 'clear-views' '[db]' 'Clear all views of Odoo database.' printf "$column" 'cloc-odoo' '[db]' 'Count custom line of codes.' printf "$column" 'drop-db' '[db]' 'Drop target Odoo database.' printf "$column" 'init-db' '[db]' 'Initialize the Odoo database.' printf "$column" 'load-language' '[db][lang]' 'Install language package in Odoo db.' printf "$column" 'patch-database' '[db][path]' 'Apply sql file to database.' printf "$column" 'reset-views' '[db][key]' 'Execute hard reset on views matching keys.' printf "$column" 'set-admin' '[db]' 'Sets the password for the first user in database.' printf "$column" 'update-module' '[db][name,path]' 'Update target Odoo module.' printf "$column" 'update-module-list' '[db]' 'Update module list of Odoo database.' # Revision printf "$column" 'checkout-latest-revision' '[version]' 'Checkout the latest revision of the Odoo version.' printf "$column" 'checkout-revision' '[revision]' 'Load Odoo revision env var and checkout git folders.' printf "$column" 'commit-and-push-revision' '[revision]' 'Commit all changes and tag with current revision.' printf "$column" 'create-revision' '[revision]' 'Create new Odoo revision.' printf "$column" 'list-revision' '' 'List available Odoo revisions.' printf "$column" 'load-latest-revision' '[version]' 'Load the latest revision of the Odoo version.' printf "$column" 'load-revision' '[revision]' 'Load env var from specified revision.' printf "$column" 'show-revision' '[revision]' 'Show references of Odoo revision.' # LLM printf "$column" 'commit-with-llm' '' 'Commit with llm generated commit message.' printf "$column" 'show-odoo-mcp-config' '' 'Show the Odoo MCP server config.' printf "$column" 'update-with-llm' '[glob][prompt]' 'Feed module files with prompt to LLM and apply file changes.' # Snippets printf "$column" 'create-snippet' '[xml-id]' 'Create snippet from template.' printf "$column" 'install-snippet' '[env][path]' 'Install snippet definition.' printf "$column" 'disable-snippet' '[env][path]' 'Disable snippet by path or xml-id.' printf "$column" 'enable-snippet' '[env][path]' 'Enable snippet by path or xml-id.' printf "$column" 'lint-snippets' '' 'Run checks for all snippets.' printf "$column" 'remove-snippet' '[env][path]' 'Remove snippet by path or xml-id.' printf "$column" 'update-snippet' '[env][path]' 'Update snippet definition.' # Setup printf "$column" 'activate-venv' '' 'Activate virtualenv.' printf "$column" 'init-venv' '' 'Initialize python virtual env.' printf "$column" 'disable-mailserver' '[env]' 'Disable mail server settings via xmlrpc.' printf "$column" 'install' '' 'Install Odoo requirements in source folder.' printf "$column" 'install-ansible-build-scripts' '[role]' 'Install scripts of the specified Ansbile role.' printf "$column" 'install-requirements' '[db][path]' 'Install python packages from requirements.txt.' printf "$column" 'list-packages' '' 'List installed python packages.' printf "$column" 'init-module' '[db][path,module]' 'Initialize Odoo module.' printf "$column" 'set-addons-path' '' 'Set Odoo addons path env variable.' # Migration printf "$column" 'export-website-data' '[env]' 'Export website data from Odoo database.' printf "$column" 'import-csv' '[db][path]' 'Import data from csv. Filename must match PostgreSQL table name.' printf "$column" 'import-website-data' '[env]' 'Import website data to Odoo database.' # Generate printf "$column" 'create-module' '[path]' 'Create new Odoo module from template.' printf "$column" 'generate-module-docs' '[path]' 'Generate readme file for module with OCA tools.' printf "$column" 'generate-module-model' '[path][model]' 'Generate model in module folder.' printf "$column" 'generate-module-inherit' '[path][model]' 'Generate inherited model in module folder.' printf "$column" 'generate-module-views' '[path][model]' 'Generate model views in module folder.' printf "$column" 'generate-module-security' '[path][model]' 'Generate model access file.' printf "$column" 'generate-module-migration' '[path]' 'Generate .' printf "$column" 'generate-module-snippet' '[path][ref][model]' 'Generate snippet for referefenced view.' printf "$column" 'generate-module-wizard' '[path][model]' 'Generate wizard for a model in module folder.' printf "$column" 'generate-module-repo' '[path]' 'Initialize Odoo module repo from template.' # Module printf "$column" 'lint-module' '[path]' 'Run pylint odoo for module.' printf "$column" 'lint-module-repo' '[path]' 'Run pylint odoo for modules in repo folder.' printf "$column" 'list-modules' '[path]' 'Get modules in path as bash array.' printf "$column" 'pytest-module' '[path]' 'Run module tests with pytest.' printf "$column" 'release-module' '[path]' 'Create GitHub release for a module.' printf "$column" 'remove-module' '[db][name]' 'Remove target Odoo module.' printf "$column" 'test-module' '[path]' 'Test target Odoo module.' printf "$column" 'test-modules' '[path]' 'Test target Odoo modules in repo folder.' printf "$column" 'translate-module' '[path][lang][db]' 'Generate translation for Odoo module.' printf "$column" 'upload-module' '[env][path]' 'Zip and upload Odoo module.' printf "$column" 'visualize-dependencies' '[path]' 'Generate visualizations of module dependencies.' printf "$column" 'zip-module' '[path]' 'Create zip file for module.' # Helper printf "$column" 'generate-admin-password' '[pass]' 'Generate hash for Odoo master password.' printf "$column" 'generate-ssh-keys' '' 'Generate ssh key pair.' printf "$column" 'generate-pg-ssl-keys' '' 'Generate PostgreSQL SSL key material.' printf "$column" 'get-addons-path' '' 'Output addons path.' printf "$column" 'get-modules' '[path][option]' 'Get list of modules in path. Option is '\''basename'\'' or '\''dirname'\''.' printf "$column" 'get-module-version' '[path]' 'Get module version from manifest.' printf "$column" 'get-module-checksum' '[path]' 'Get module checksum.' printf "$column" 'help' '[grep]' 'Show help for commands.' printf "$column" 'info' '' 'Show values of project env vars.' printf "$column" 'load-ssh-key' '' 'Load SSH private key from env var.' printf "$column" 'update-module-license' '[grep]' 'Update LICENSE file for each matching module.' # Run printf "$column" 'debug' '[name]' 'Debugg application. Options: source.' printf "$column" 'exec' '[name][cmd]' 'Run command in container.' printf "$column" 'logs' '[name]' 'Tail container logs. Default is '\''odoo'\''.' printf "$column" 'odoocli' '[param]' 'Execute odoocli cli.' printf "$column" 'psql' '[db]' 'Start interactive psql shell.' printf "$column" 'restart' '[name]' 'Restart container.' printf "$column" 'run' '[name][cmd]' 'Run container with command.' printf "$column" 'shell' '[db][code]' 'Start interactive odoo shell or run code.' printf "$column" 'source' '' 'Source the Python virtual env.' printf "$column" 'start' '[name][db]' 'Start application. Options: none, admin, db, mailgate, mailpit, source, odoo, hatch.' printf "$column" 'stop' '[name]' 'Stop containers.' printf "$column" 'test-xmlrpc' '[env]' 'Test json rpc connection.' # Project printf "$column" 'lint' '' 'Run precommit for this project.' printf "$column" 'list-versions' '' 'List available Odoo versions.' printf "$column" 'check-version' '' 'Check if Odoo source is the correct version.' printf "$column" 'load-version' '[version]' 'Load git refs from version folder.' printf "$column" 'save-version' '' 'Save git folder refs to version folder.' printf "$column" 'template-compose' '' 'Template the Docker compose file.' printf "$column" 'template-odoo-rc' '' 'Template the Odoo config file.' printf "$column" 'template-repo' '[path]' 'Update the repo folder from template.' printf "$column" 'test-project' '[cleanup]' 'Run tests for this project.' printf "$column" 'version' '' 'Show version of required tools.' # Performance printf "$column" 'record-with-memray' '[name]' 'Record application memory usage with memray. Options: source.' printf "$column" 'record-with-py-spy' '[pid]' 'Record and create flamechart for a process.' # Docs printf "$column" 'update-docs' '' 'Update all project docs.' printf "$column" 'update-help-doc' '' 'Write help table to task.md file.' printf "$column" 'update-modules-doc' '' 'Update modules docs file.' printf "$column" 'update-revisions-doc' '' 'Update revisions doc file.' printf "$column" 'update-snippets-doc' '' 'Update snippets doc file.' # Upgrade printf "$column" 'migrate-module' '[path]' 'Migrate module code from to target Odoo version.' printf "$column" 'upgrade-odoo' '[env][step]' 'Run the Odoo upgrade for a target environment.' } 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 PASS_ENTRY=$(echo "env/${PWD##*/}" | tr '[:upper:]' '[:lower:]') 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 ORG_NAME=${ORG_NAME:=Mint-System} ODOO_DATABASE=${ODOO_DATABASE:="$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:=1} 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" fi # Check commands if ! (command -v grep &> /dev/null && grep --version | grep -q GNU) && [[ "$OS_RELEASE" =~ ^(Darwin)$ ]]; then echo 'Error: On Darwin systems, GNU grep must be installed.' exit 1 fi if ! command -v envsubst &> /dev/null; then echo 'Error: '\''envsubst'\'' is not available. Install '\''gettext'\'' package.' exit 1 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 "Pass Entry: $PASS_ENTRY" 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 "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 } show-odoo-mcp-config() { echo 'Add this config to your IDE: ' cat << EOF { "mcpServers": { "odoo": { "command": "$PWD/venv$ODOO_VERSION/bin/python", "args": ["$PWD/images/odoo-mcp/bin/server"], "env": { "ODOO_URL": "http://localhost:8069", "ODOO_DATABASE": "$ODOO_DATABASE", "ODOO_USERNAME": "admin", "ODOO_PASSWORD": "admin" } } } } EOF } # 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/,*$//g')" # 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") done # Check if addons dir exist while read -r addon; do # Trim whitespace addon=$(echo "$addon" | xargs) # Skip empty lines if [[ -z "$addon" ]]; then continue fi # Check if directory exists if [[ ! -d "$addon" ]]; then echo "Error: Path does not exist: $addon" exit 1 fi done < <(echo "$git_folders_path" | tr ',' '\n') # Convert to source paths 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 to make it available to other functions 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 } update-help-doc() { echo 'Pipe help table into task.md.' help-table > task.md } 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/${ORG_NAME}/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-help-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 } template-repo(){ 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=$(grep -m 1 \"version "$1/__manifest__.py" | sed "s;';\";g" | sed "s/,//g" | sed 's/#.*//g') version=$(echo "{ $version }" | jq .version | sed 's/"//g' | sed 's/null//g') # 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 envsubst < "compose.yml.template" > "compose.yml" } container-ps() { $CONTAINER_ENGINE ps } build() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi # Set container tag based on Dockerfile local manifest_file local latest_tag if [[ "$1" == "images/odoo" ]]; then manifest_file="images/odoo/Dockerfile" latest_tag="odoo:$ODOO_VERSION" fi if [[ "$1" == "images/odoo-mailgate" ]]; then manifest_file="images/odoo-mailgate/Dockerfile" CONTAINER_TAG="odoo-mailgate" latest_tag="odoo-mailgate:latest" elif [[ "$1" == "images/odoo-upgrade" ]]; then manifest_file="images/odoo-upgrade/Dockerfile" CONTAINER_TAG="odoo-upgrade" latest_tag="odoo-upgrade:latest" elif [[ "$1" == "images/odoocli" ]]; then manifest_file="images/odoocli/Dockerfile" CONTAINER_TAG="odoocli" latest_tag="odoocli:latest" elif [[ "$1" == "images/odoo-mcp" ]]; then manifest_file="images/odoo-mcp/Dockerfile" CONTAINER_TAG="odoo-mcp" latest_tag="odoo-mcp:latest" fi # Load Odoo revision local odoo_date=$(echo "$ODOO_REVISION" | cut -d'.' -f3-) source "revisions/$ODOO_REVISION" # Select Python version 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 "$2" ]]; then docker_output="$2" fi # Output configuration echo "Python Version: $python_version" 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" \ --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}/${latest_tag}" \ "$docker_output" } 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 } 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 | 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() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ "$1" == "images/odoo-upgrade" ]]; then CONTAINER_TAG="odoo-upgrade" elif [[ "$1" == "images/odoocli" ]]; then CONTAINER_TAG="odoocli" 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 = "test-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 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" =~ ^odoocli ]]; then echo -e "\033[38;5;214mTEST\033[0m: Show help of odoocli." docker_odoocli() { docker run -it \ -e ODOO_URL="$ODOO_URL" -e ODOO_DATABASE="$ODOO_DATABASE" -e ODOO_USERNAME="$ODOO_USERNAME" -e ODOO_PASSWORD="$ODOO_PASSWORD" \ mintsystem/odoocli "$@" } docker_odoocli --help fi } push() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local latest_tag if [[ "$1" == "images/odoo" ]]; then latest_tag="odoo:$ODOO_VERSION" elif [[ "$1" == "images/odoo-mailgate" ]]; then CONTAINER_TAG="odoo-mailgate" latest_tag="odoo-mailgate:latest" elif [[ "$1" == "images/odoo-upgrade" ]]; then CONTAINER_TAG="odoo-upgrade" latest_tag="odoo-upgrade:latest" elif [[ "$1" == "images/odoocli" ]]; then CONTAINER_TAG="odoocli" latest_tag="odoocli:latest" elif [[ "$1" == "images/odoo-mcp" ]]; then CONTAINER_TAG="odoo-mcp" latest_tag="odoocli:latest" fi echo "Tag container image ${CONTAINER_REGISTRY}/${CONTAINER_TAG} with ${CONTAINER_REGISTRY}/${latest_tag}" $CONTAINER_ENGINE tag "${CONTAINER_REGISTRY}/${CONTAINER_TAG}" "${CONTAINER_REGISTRY}/${latest_tag}" if [[ "$CONTAINER_ENGINE" == "docker" ]]; then echo "Push container image to ${CONTAINER_REGISTRY}/${CONTAINER_TAG}" docker --config "$CONTAINER_CONFIG" push "${CONTAINER_REGISTRY}/${CONTAINER_TAG}" docker --config "$CONTAINER_CONFIG" push "${CONTAINER_REGISTRY}/${latest_tag}" fi if [[ "$CONTAINER_ENGINE" == "podman" ]]; then echo "Push container image to ${CONTAINER_REGISTRY}/${CONTAINER_TAG}" podman push "${CONTAINER_REGISTRY}/${CONTAINER_TAG}" podman push "${CONTAINER_REGISTRY}/${latest_tag}" 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-env "$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 template-compose # Use default database if second param is not given local database="$2" if [[ -z "$2" ]]; then database="$ODOO_DATABASE" 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" =~ "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() { 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-env 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 ODOO_DATABASE=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: 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 echo -e "\033[38;5;214mTEST\033[0m: Init OCA module." run 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_USERNAME=test export ODOO_PASSWORD=test if ! show-env test; then create-env test odoo fi show-env 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 run 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 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 "Run 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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-env "$1" images/odoocli/bin/odoocli --method 'write' --model 'ir.mail_server' --field 'active' --value False --domain "[]" images/odoocli/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-env "$1" local uuid=$(uuidgen) echo "Set uuid $uuid for $1." images/odoocli/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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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/odoo-module-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="$ODOO_DATABASE" 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 } generate-module-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" cd "$repo_path" || exit git switch --orphan -c "$ODOO_VERSION" echo "Remove all files from $repo_path" rm -rf "$repo_path/*" fi echo "Copy template files from templates/$ODOO_VERSION to $repo_path" cp "templates/$ODOO_VERSION/*" "$repo_path" } lint-module-repo() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi local module_paths=$(get-modules "$MODULE_PATH") for module_path in ${module_paths//,/ }; do lint-module "$module_path" done } # 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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="$ODOO_DATABASE" 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 } lint-module() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi echo "Set file and folder permissions on $1" find "$1" -type d -exec chmod u=rwx,go=rx {} \; find "$1" -type f -exec chmod u=rw,go=r {} \; echo 'Update index.html' rst2html5 "$1/README.rst" "$1/static/description/index.html" pushd . cd "$1" echo "Run pre-commit in $PWD" pre-commit run --all-files # --show-diff-on-failure --color=always echo 'Stage changes' git add . popd } 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 "$ODOO_DATABASE" -i "$module" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http --test-tags /"$module_name" } 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 "$ODOO_DATABASE" -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 "$ODOO_DATABASE" -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="$ODOO_DATABASE" "$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="$ODOO_DATABASE" 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_name=$(basename "$1") local parent_dir=$(dirname "$1") echo "Scaffolding module $module_name in ${parent_dir}..." odoo scaffold "$module_name" "$parent_dir" -t templates/module } 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 "Generate OCA readme for $ORG_NAME org." local repo_url=$(git -C "$(dirname $module_path)" remote get-url origin) local repo_name=$(basename "${repo_url%.git}") oca-gen-addon-readme --repo-name=$repo_name --branch=$ODOO_VERSION --org-name=$ORG_NAME --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" } 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" local module_name=$(basename "$1") echo 'Substitute model template' 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 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-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 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 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 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 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 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi if [[ -z "$3" ]]; then echo '$3 is empty.' exit fi local module_path="$1" # addons/sale_workflow/sale_helm echo 'Substitute snippet template' export MODEL_DOT_NAME="$3" # sale.order export VIEW_REF="$2" # sale.view_order_form export MODEL_NAME=${MODEL_DOT_NAME//\./_} export MODULE_NAME=$(basename "$1") export SRC_MODULE_NAME=$(echo "$VIEW_REF" | cut -d"." -f1) export XML_ID=$(echo "$VIEW_REF" | cut -d"." -f2) mkdir -p "$module_path/views" envsubst < templates/model_snippet.xml > "$module_path/views/${MODEL_NAME}_views.xml" echo "Update file $MODULE_NAME/__manifest__.py with: \"views/${MODEL_NAME}_views.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 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 directory=$(dirname "$module_path") local module=$(basename "$module_path") # if [[ "$ODOO_VERSION" == "17.0" ]]; then # echo "Migrate views of module $module_path to ${ODOO_VERSION}..." # odoo --database "$DATABASE" -i "$module_path" --config "$ODOO_RC" --addons-path="$ADDONS_PATH" --load=base,web,views_migration_17 --stop-after-init # fi # if [[ "$ODOO_VERSION" == "18.0" ]]; then # echo "Run Odoo upgrade_code command for $module_path from ${source_odoo_version}..." # odoo upgrade_code --addons-path "$MODULE" --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" # else 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() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi load-env "$1" temp_folder="/var/tmp" if [[ "$2" =~ "list" ]]; then echo "all-test" echo "all-production" echo "dump" echo "filestore" echo "drop" echo "test" echo "production" echo "init" echo "uninstall" echo "auto-update" echo "update" echo "configure-test" echo "configure-production" echo "restart" echo "logs" echo "shell" fi if [[ "$2" =~ "dump" ]]; then log-warning "Run step 'dump' on $SERVER." ssh -p $PORT "$SERVER" sudo docker-postgres-backup -c "$POSTGRES_CONTAINER" -d "$DATABASE" -o "$temp_folder" if [[ "$SERVER" != "$TARGET_SERVER" ]]; then log-warning "Source and target host are different." echo "Copy dump from $SERVER to $TARGET_SERVER..." ssh -p $TARGET_PORT "$TARGET_SERVER" mkdir -p "$temp_folder/$TARGET_POSTGRES_CONTAINER" scp "$SERVER:$temp_folder/$POSTGRES_CONTAINER/$DATABASE.sql" "$TARGET_SERVER:$temp_folder/$TARGET_POSTGRES_CONTAINER/$DATABASE.sql" ssh -p $TARGET_PORT "$TARGET_SERVER" sudo docker-postgres-restore -c "$TARGET_POSTGRES_CONTAINER" -d "$DATABASE" -f "$temp_folder/$TARGET_POSTGRES_CONTAINER/$DATABASE.sql" -r else ssh -p $TARGET_PORT "$TARGET_SERVER" sudo docker-postgres-restore -c "$TARGET_POSTGRES_CONTAINER" -d "$DATABASE" -f "$temp_folder/$POSTGRES_CONTAINER/$DATABASE.sql" -r fi fi if [[ "$2" =~ "filestore" ]]; then log-warning "Run step 'filestore' on $SERVER." ssh -p $PORT "$SERVER" docker-export -c "$ODOO_CONTAINER" -p "/var/lib/odoo/filestore/$DATABASE" -o "$temp_folder" if [[ "$SERVER" != "$TARGET_SERVER" ]]; then log-warning "Source and target host are different." echo "Copy filestore from $SERVER to $TARGET_SERVER..." scp "$SERVER:$temp_folder/$ODOO_CONTAINER.tar" "$TARGET_SERVER:$temp_folder/$TARGET_ODOO_CONTAINER.tar" ssh -p $TARGET_PORT "$TARGET_SERVER" docker-import -c "$TARGET_ODOO_CONTAINER" -p "/var/lib/odoo/filestore/$TARGET_DATABASE" -f "$temp_folder/$TARGET_ODOO_CONTAINER.tar" -r else ssh -p $TARGET_PORT "$TARGET_SERVER" docker-import -c "$TARGET_ODOO_CONTAINER" -p "/var/lib/odoo/filestore/$TARGET_DATABASE" -f "$temp_folder/$ODOO_CONTAINER.tar" -r fi fi if [[ "$2" =~ "drop" ]]; then log-warning "Run step 'drop' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker-postgres-drop -c "$TARGET_POSTGRES_CONTAINER" -d "$TARGET_DATABASE" fi if [[ "$2" =~ "test" ]]; then log-warning "Run step 'test' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker-odoo-upgrade -c "$TARGET_POSTGRES_CONTAINER" -d "$DATABASE" -s "$ODOO_VERSION" -n "$TARGET_DATABASE" -t "$TARGET_ODOO_VERSION" fi if [[ "$2" =~ "init" ]]; then log-warning "Run step 'init' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker-odoo-init -c "$TARGET_ODOO_CONTAINER" -d "$TARGET_DATABASE" -i "$ODOO_ADDONS_INIT" fi if [[ "$2" =~ "uninstall" ]]; then log-warning "Run step 'uninstall' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker-odoo-uninstall -c "$TARGET_ODOO_CONTAINER" -d "$TARGET_DATABASE" -u "$ODOO_ADDONS_UNINSTALL" fi if [[ "$2" =~ "auto-update" ]]; then log-warning "Run step 'auto-update' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker-odoo-shell -c "$TARGET_ODOO_CONTAINER" -d "$TARGET_DATABASE" -f -p "env['ir.module.module'].upgrade_changed_checksum()" fi if [[ "$2" =~ "update" ]]; then log-warning "Run step 'update' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker-odoo-update -c "$TARGET_ODOO_CONTAINER" -d "$TARGET_DATABASE" -u all ssh -p "$TARGET_PORT" "$TARGET_SERVER" docker-odoo-clear-assets -c "$TARGET_ODOO_CONTAINER" -d "$TARGET_DATABASE" fi if [[ "$2" =~ "configure-test" ]]; then log-warning "Run step 'configure' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker-odoo-shell -c "$TARGET_ODOO_CONTAINER" -d "$TARGET_DATABASE" -f -p "\"$ODOO_CONFIGURE_TEST\"" fi if [[ "$2" =~ "configure-production" ]]; then log-warning "Run step 'configure' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker-odoo-shell -c "$TARGET_ODOO_CONTAINER" -d "$TARGET_DATABASE" -f -p "\"$ODOO_CONFIGURE_PRODUCTION\"" fi if [[ "$2" =~ "restart" ]]; then log-warning "Run step 'restart' on $TARGET_SERVER." ssh -p $TARGET_PORT "$TARGET_SERVER" docker restart "$TARGET_ODOO_CONTAINER" "$TARGET_POSTGRES_CONTAINER" fi if [[ "$2" =~ "logs" ]]; then log-warning "Run step 'logs' on $TARGET_SERVER." containe_id=$(ssh -p $TARGET_PORT "$TARGET_SERVER" docker ps -f "name=$TARGET_ODOO_CONTAINER" --format '{{.ID}}' | head -n1) ssh -p $TARGET_PORT "$TARGET_SERVER" docker logs -f "$containe_id" --tail 10 fi if [[ "$2" =~ "shell" ]]; then log-warning "Run step 'shell' on $TARGET_SERVER." #FIXME: Command fails with "the input device is not a TTY" echo "ssh -p $TARGET_PORT \"$TARGET_SERVER\"" echo docker-odoo-shell -c \"$TARGET_ODOO_CONTAINER\" -d \"$TARGET_DATABASE\" fi } # 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="$ODOO_DATABASE" 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 'cat //data/@inherit_id' | xmllint --shell "$2" | awk -F'[="]' '!/>/{print $(NF-1)}') 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)}') local module_name=$(echo "$inherit_id" | cut -d. -f 1 ) local xml_id=$(echo "$inherit_id" | cut -d. -f 2 ) # Set default values : ${type:='qweb'} load-env "$1" echo "Search for view with name $module_name.$xml_id" inherit_id=$(images/odoocli/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/odoocli/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-env "$1" echo "Update view with name $name" images/odoocli/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-env "$1" echo "Disable view with name $name" images/odoocli/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-env "$1" echo "Enable view with name $name" images/odoocli/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-env "$1" echo "Remove view with name $name" images/odoocli/bin/odoocli --method "unlink" --model "ir.ui.view" --domain "[('name', '=', '$name')]" } lint-snippets() { echo 'Lint snippets files...' local files="snippets/*.xml" for file in $files; do # Access various snippet parameters local filename=$(basename "$file") local snippet_module=$(echo "$filename" | cut -d. -f2) local report=$(echo "$filename" | cut -d. -f3) local edit=$(echo "$filename" | cut -d. -f4) local inherit_id=$(echo 'cat //data/@inherit_id' | xmllint --shell "$file" | awk -F'[="]' '!/>/{print $(NF-1)}') local type=$(echo 'cat //data/@type' | xmllint --shell "$file" | awk -F'[="]' '!/>/{print $(NF-1)}') local model=$(echo 'cat //data/@model' | xmllint --shell "$file" | awk -F'[="]' '!/>/{print $(NF-1)}') local priority=$(echo 'cat //data/@priority' | xmllint --shell "$file" | awk -F'[="]' '!/>/{print $(NF-1)}') local module_name=$(echo "$inherit_id" | cut -d. -f 1 ) local xml_id=$(echo "$inherit_id" | cut -d. -f 2 ) # Check filename local count_dots=$(echo "$filename" | grep -o "\." | wc -l) if [[ "$count_dots" -ge 5 ]]; then echo "Linting failed for $file" echo "Dots count: $count_dots" echo " Filename: $snippet_module.$report" exit 1 fi # Check if inherit id matches the filename if [[ -n "$inherit_id" ]]; then if [[ "$inherit_id" != "$snippet_module.$report" ]]; then echo "Linting failed for $file" echo " Inherit: $inherit_id" echo "Filename: $snippet_module.$report" exit 1 fi fi # Check if priority is set if [[ -z "$priority" ]] && [[ -n "$inherit_id" ]]; then echo "Linting failed for $file" echo 'No priority is set.' exit 1 fi done echo 'No problems with snippets found.' } # Git commands add-git-submodule() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi if [[ -z "$2" ]]; then echo '$2 is empty.' exit fi # Add to .gitmodules git submodule add -f -b "$ODOO_VERSION" "$1" "$2" # Ensure branch is Odoo version git -C "$2" switch "$ODOO_VERSION" # Save version save-version # Restore staged submodule files git restore --staged .gitmodules "$2" # Remove from working tree # rm -rf .git/modules/"$2" || true # git rm --cached "$2" || true } aggregate-git-folders() { if [[ -z "$1" ]]; then echo '$1 is empty.' exit fi gitaggregate -c "$1" --expand-env } ls-git-folder() { if [[ -n "$1" ]]; then echo $(git config --file .gitmodules --get-regexp url | sed -e 's/submodule.//g' -e 's/.url.*//g' | grep "$1" | sort) else echo $(git config --file .gitmodules --get-regexp url | sed -e 's/submodule.//g' -e 's/.url.*//g' | sort) fi } list-git-folder() { # Define column widths local path_width=35 local url_width=50 local branch_width=8 # Print table header printf "| %-${path_width}s | %-${url_width}s | %-${branch_width}s |\n" "PATH" "URL" "BRANCH" echo "|$(printf '%*s' $((path_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((url_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((branch_width + 2)) '' | tr ' ' '-')|" local git_folders=$(ls-git-folder "$1") for git_folder in $git_folders; do local url=$(git config --file .gitmodules --get "submodule.$git_folder.url") url=${url#git@github.com:} url=${url%.git} local branch=$(git -C "$git_folder" branch --show-current 2>/dev/null || echo "detached") printf "| %-${path_width}s | %-${url_width}s | %-${branch_width}s |\n" "$git_folder" "$url" "${branch:-detached}" done } remove-git-folders() { local git_folders=$(ls-git-folder) echo "Delete: $git_folders" read -p "Are you sure you want to delete these git folders? (yes/N): " confirm if [[ "${confirm,,}" != "yes" ]]; then echo 'Aborted.' return 1 fi for git_folder in $git_folders; do echo "Delete git folder $git_folder." rm -rf "$git_folder" done } remove-git-submodule() { if [[ -z "$1" ]]; then echo "\$1 is empty."; exit; fi echo 'Deinit submodule' git submodule deinit -f "$1" echo 'Remove submodule from work tree' git rm -f "$1" echo 'Remove submodule from .git folder' rm -rf ".git/modules/$1" echo 'Remove submodule from .gitmodules file' git config --file .gitmodules --remove-section submodule.addons/website save-version } clone-git-folder() { local git_folders=$(ls-git-folder "$1") for git_folder in $git_folders; do local git_folder_path=$(git config --file .gitmodules --get submodule."$git_folder".path) local git_folder_url=$(git config --file .gitmodules --get submodule."$git_folder".url) local shallow=$(git config --file .gitmodules --get submodule."$git_folder".shallow) local git_folder_branch=$(git config --file .gitmodules --get submodule."$git_folder".branch) if [[ "$shallow" = "true" ]]; then echo "Clone git folder $git_folder (shallow) with $git_folder_branch branch" git -C "$git_folder" config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" git clone --depth 1 --no-single-branch "$git_folder_url" "$git_folder_path" else echo "Clone git folder $git_folder with $git_folder_branch branch" git clone --branch="$git_folder_branch" "$git_folder_url" "$git_folder_path" fi done } download-git-folder() { local git_folders=$(ls-git-folder "$1") for git_folder in $git_folders; do local git_folder_path=$(git config --file .gitmodules --get submodule."$git_folder".path) local git_folder_url=$(git config --file .gitmodules --get submodule."$git_folder".url) local git_folder_branch=$(git config --file .gitmodules --get submodule."$git_folder".branch) # Convert git url to http url. git_folder_url="$(echo $git_folder_url | sed 's|git@|https://|g' | sed 's|m:|m/|g' | sed 's|\.git||g')" # Get commit ref from .gitmodule.csv local commit_ref=$(grep "$git_folder_path" "versions/$ODOO_VERSION/.gitmodules.csv" | cut -d',' -f2) echo "Download $git_folder_url to $git_folder_path." mkdir -p "$git_folder_path" curl -L -o "$git_folder_path/archive.tar.gz" \ "$git_folder_url/archive/$commit_ref.tar.gz" echo "Extract and delete $git_folder_path/archive.tar.gz" tar -xzf "$git_folder_path/archive.tar.gz" --strip-components=1 -C "$git_folder_path" rm "$git_folder_path/archive.tar.gz" done } checkout-git-folder() { echo 'Checkout submodule commits.' local git_folders=$(ls-git-folder) for git_folder in $git_folders; do local shallow=$(git config --file .gitmodules --get submodule."$git_folder".shallow) local commit_ref=$(grep "$git_folder" "versions/$ODOO_VERSION/.gitmodules.csv" | cut -d',' -f2) if [[ "$shallow" = "true" ]]; then echo "Checkout git folder $git_folder (shallow) to $commit_ref" git -C "$git_folder" stash git -C "$git_folder" fetch origin "$ODOO_VERSION:$ODOO_VERSION" git -C "$git_folder" checkout "$commit_ref" else echo "Checkout git folder $git_folder to $commit_ref" git -C "$git_folder" stash git -C "$git_folder" checkout "$commit_ref" fi done } switch-git-folder() { if [[ -n "$1" ]]; then ODOO_VERSION="$1" fi local git_folders=$(ls-git-folder) for git_folder in $git_folders; do local shallow=$(git config --file .gitmodules --get submodule."$git_folder".shallow) if [[ "$shallow" = "true" ]]; then echo "Fetch branch $ODOO_VERSION (shallow) from origin and then switch." git -C "$git_folder" fetch origin "$ODOO_VERSION:$ODOO_VERSION" git -C "$git_folder" switch "$ODOO_VERSION" else echo "Switch git folder branch to $ODOO_VERSION: $git_folder" git -C "$git_folder" stash git -C "$git_folder" switch "$ODOO_VERSION" fi done } pull-git-folder() { local git_folders=$(ls-git-folder) for git_folder in $git_folders; do echo "Pull git folder $git_folder." git -C "$git_folder" stash local update=$(git config submodule."$git_folder".update) if [[ -z "$update" ]]; then git -C "$git_folder" pull origin "$ODOO_VERSION" fi; done } sync-git-folder() { echo 'Switch and pull submodules.' switch-git-folder pull-git-folder } create-git-feature-branch(){ if [[ -z "$1" ]]; then echo "\$1 is empty."; exit; fi local module_name=$(basename "$1") local module_path=$(dirname "$1") echo "Create feature branch for $module_name in ${module_path}..." cd ./"$module_path" || exit pwd git switch -c "feature-$module_name" } create-git-mig-branch(){ if [[ -z "$1" ]]; then echo "\$1 is empty."; exit; fi local module_name=$(basename "$1") local module_path=$(dirname "$1") echo "Create mig branch for $module_name in ${module_path}..." cd ./"$module_path" || exit pwd git switch -c "$ODOO_VERSION-mig-$module_name" } # Project commands load-version() { if [[ -n "$1" ]]; then ODOO_VERSION="$1" fi echo "Copy .gitmodules and .python-version from 'versions/$ODOO_VERSION'." cp "versions/$ODOO_VERSION/.gitmodules" . 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 # Upate branch switch-git-folder } save-version() { local git_folders_file="versions/$ODOO_VERSION/.gitmodules.csv" echo "Save submodule refs to $git_folders_file." local git_folders=$(ls-git-folder) echo "path,rev" > "$git_folders_file" for git_folder in $git_folders; do local commit_ref=$(git -C "$git_folder" rev-parse HEAD) echo "$git_folder,$commit_ref" >> "$git_folders_file" done echo "Copy .gitmodules and .python-version to 'versions/$ODOO_VERSION'" cp ".gitmodules" "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-env "$1" echo "Connect to database and count res.partner records: $(images/odoocli/bin/odoocli --model 'res.partner' --method 'search_count' --domain '[]')" } odoocli(){ images/odoocli/bin/odoocli "$@" } export-website-data() { if [[ -n "$1" ]]; then load-env $1 fi export ODOO_URL export ODOO_DATABASE 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-env $1 fi export ODOO_URL export ODOO_DATABASE 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 declare -f "$1" > /dev/null; then activate-venv "$1" "${@:2}" else case "$1" in source) activate-venv ;; *) echo "Unknown command: $1" help exit 1 ;; esac fi