Files
Odoo-Build/task
T
Janik von Rotz d52b2cc4f2 feat(create-module): add interactive test instructions prompt and demo.xml template
Enhance create-module to interactively prompt for test instructions, creating a tests/TEST_INSTRUCTIONS.rst file if requested. Add demo.xml template for demo data records and automatically create demo directory when demo is enabled. Update demo handling to use the new template file instead of hardcoded content.
2026-03-06 17:01:42 +01:00

3096 lines
102 KiB
Bash
Executable File

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