Files
Odoo-Build/task
T

3129 lines
100 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+=("copy-dotenv|<env><env>|Copy .env file.")
rows+=("create-dotenv|<env><type>|Create .env file of type nextcloud, odoo, upgrade.")
rows+=("edit-dotenv|<env>|Open .env file in default editor.")
rows+=("list-dotenv||List .env files.")
rows+=("load-dotenv|<env>|Load and export .env file.")
rows+=("remove-dotenv|<env>|Remove environment config.")
rows+=("rename-dotenv|<env><env>|Rename .env file.")
rows+=("restore-dotenv-files|<path>|Extract and copy .env files from backup file.")
rows+=("show-dotenv|<env>|Output content of the .env file.")
rows+=("===GIT===| | ")
rows+=("aggregate-git-folders|<path>|Run gitaggregate for the repos.yaml.")
rows+=("add-git-folder|<branch><url><path>|Add git folder to .gitmodules.")
rows+=("checkout|<version>|Checkout Odoo version.")
rows+=("checkout-git-folder|<branch><path>|Checkout git commit from .gitmodules.")
rows+=("commit-git-folder|<message><path>|Commit all changes in path.")
rows+=("clean-git-folder|<path>|Clean git folder.")
rows+=("clone-git-folder|<branch>|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|<path>|Push all git folders in path.")
rows+=("remove-git-folder|<path>|Remove git folders in path.")
rows+=("ls-git-folder|<grep>|List git folders path space separated.")
rows+=("reset-git-folder|<path>|Abort rebase and reset git folder.")
rows+=("stage-git-folder|<path>|Stage all files in git folders in path.")
rows+=("switch-git-folder|<branch><path>|Switch branch for all git folders from the .gitmodules file.")
rows+=("sync-git-folder|<branch><path>|Switch, stash and pull all git folders.")
rows+=("run-pre-commit|<path>|Run pre-commit in path.")
rows+=("===DOCKER===| | ")
rows+=("archive-docker-tags||Archive Docker image tags on hub older than 1 year.")
rows+=("build-odoo|[output]|Build Odoo Docker image.")
rows+=("build-odoo-mailgate|[output]|Build Odoo Mailgate Docker image.")
rows+=("build-odoo-upgrade|[output]|Build Odoo Upgrade Docker image.")
rows+=("build-odoo-cli|[output]|Build Odoo CLI Docker image.")
rows+=("login-docker|[user][token]|Setup Docker Hub login credentials.")
rows+=("login-podman|[user][token]|Login into registry with podman.")
rows+=("update-docker-hub-readme||Update Docker Hub repository READMEs from image folders.")
rows+=("container-ps||List container processes.")
rows+=("remove|[name]|Remove containers and volumes.")
rows+=("test-image-script|<folder>|Test scripts of target Docker image 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+=("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+=("install||Install Odoo requirements in source folder.")
rows+=("install-ansible-build-scripts|<role>|Install scripts of the specified Ansbile role.")
rows+=("install-requirements|<db>[path]|Install python packages from requirements.txt.")
rows+=("list-packages||List installed python packages.")
rows+=("init-module|<db>[path,module]|Initialize Odoo module.")
rows+=("set-addons-path||Set Odoo addons path env variable.")
rows+=("===DATA===| | ")
rows+=("export-website-data|[env]|Export website data from Odoo database.")
rows+=("import-csv|<db>[path]|Import data from csv. Filename is PostgreSQL table name.")
rows+=("import-website-data|[env]|Import website data to Odoo database.")
rows+=("===DEVELOP===| | ")
rows+=("create-module|<path>|Create new Odoo module from template.")
rows+=("create-repo|<path>|Create a new Odoo module repo.")
rows+=("init-repo-branch|<path>|Create branch for current Odoo version in repo.")
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-model|<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-inherit-view|<path><ref><model>|Generate snippet for referefenced view.")
rows+=("generate-module-report|<path><model>|Generate report for a model in module folder.")
rows+=("generate-module-translation|<path>|Generate module translation and update with llm.")
rows+=("generate-module-wizard|<path><model>|Generate wizard for a model in module folder.")
rows+=("generate-repo-docs|<path>|Generate repo files from template.")
rows+=("===MODULE===| | ")
rows+=("list-modules|<path>|Get modules in path as bash array.")
rows+=("pytest-module|<path>|Run module tests with pytest.")
rows+=("release-module|<path>|Create GitHub release for a module.")
rows+=("register-module-repos|[version][path]|Register module repos on apps.odoo.com.")
rows+=("uninstall-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+=("list-services||List available services.")
rows+=("debug|<name>|Debug application.")
rows+=("exec|<name><cmd>|Run command in container.")
rows+=("logs|[name]|Tail container logs. Default is '\''odoo'\''.")
rows+=("manifestoo|[param]|Execute manifestoo cli.")
rows+=("odoobin|[param]|Execute odoo binary.")
rows+=("odoocli|[param]|Execute odoocli cli.")
rows+=("psql|[db]|Start interactive psql shell.")
rows+=("restart|[name]|Restart container.")
rows+=("run|<name><cmd>|Run container with command.")
rows+=("shell|[db][code]|Start interactive odoo shell or run code.")
rows+=("source||Source the Python virtual env.")
rows+=("start|[name][db]|Start service.")
rows+=("stop|[name]|Stop containers.")
rows+=("test-xmlrpc|<env>|Test xml rpc connection.")
rows+=("test-json-2|<env>|Test JSON API connection.")
rows+=("===PROJECT===| | ")
rows+=("lint||Run precommit for this project.")
rows+=("list-versions||List available Odoo versions.")
rows+=("check-version||Check if Odoo source is the correct version.")
rows+=("load-version|[version]|Load git refs from version folder.")
rows+=("save-version||Save git folder refs to version folder.")
rows+=("template-compose||Template the Docker compose file.")
rows+=("template-odoo-rc||Template the Odoo config file.")
rows+=("test-project|[cleanup]|Run tests for this project.")
rows+=("version||Show version of required tools.")
rows+=("===PERFORMANCE===| | ")
rows+=("record-with-memray|<name>|Record application memory usage with memray.")
rows+=("record-with-py-spy|<pid>|Record and create flamechart for a process.")
rows+=("===DOC===| | ")
rows+=("update-docs||Update all project docs.")
rows+=("update-modules-doc||Update modules docs file.")
rows+=("update-revisions-doc||Update revisions doc file.")
rows+=("update-snippets-doc||Update snippets doc file.")
rows+=("===UPGRADE===| | ")
rows+=("prepare-migration-brach|<path><module><version>|Create migration branch for a module.")
rows+=("migrate-module|<path>|Migrate module code to target Odoo version.")
rows+=("backport-module|<path>|Backport module code to target Odoo version.")
rows+=("run-openupgrade|<db>|Run Odoo OpenUpgrade scripts for target database.")
rows+=("===JUPYTER===| | ")
rows+=("init-odoo-jupyter|<path>|Init jypyter lab directory.")
rows+=("start-odoo-jupyter|<path>[db]|Start juypter lab in directory.")
rows+=("exec-odoo-jupyter|<path>[db]|Run juypter notebook with papermill.")
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
ODOO_RC="odoo.conf"
export PYTHONPATH="$PWD/odoo" # Required so upgrade-util does not overwrite the odoo namespace
ODOO_VERSION=$(echo "$ODOO_REVISION" | cut -d'.' -f1-2)
CONTAINER_CONFIG="$HOME/.docker/$CONTAINER_REGISTRY"
# Updateable env vars
USER_ORG=${USER_ORG:=Mint-System}
DB_NAME=${DB_NAME:="$ODOO_VERSION"}
WITHOUT_DEMO=${WITHOUT_DEMO:=False}
ODOO_LANGUAGE=${ODOO_LANGUAGE:=de_CH}
ODOO_PORT=${ODOO_PORT:=8069}
NGINX_PORT=${NGINX_PORT:=8080}
JPYTER_LAB_PORT=${JPYTER_LAB_PORT:=8888}
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}
CONTAINER_NETWORK=${CONTAINER_NETWORK:=odoo-build_default}
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}
PROXY_MODE=${PROXY_MODE:=False}
# Conditional env vars
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
COMPOSE_COMMAND="podman-compose"
fi
if [[ "$(uname)" == "Darwin" ]]; then
OS_RELEASE="Darwin"
PYTHON_NOTIFY=""
else
OS_RELEASE=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"')
PYTHON_NOTIFY="inotify"
fi
if [[ "$WITHOUT_DEMO" = "True" ]]; then
ODOO_PARAM="--without-demo=all"
elif [[ "$ODOO_VERSION" = "19.0" ]]; then
ODOO_PARAM="--with-demo"
fi
# Import commands
clone-taskfile(){
if [[ ! -d "$HOME/taskfile.build" ]]; then
echo -e '\033[38;5;214mGit\033[0m: Clone taskfile repo'
git clone https://git.taskfile.build "$HOME/taskfile.build"
else
echo -e '\033[38;5;214mGit\033[0m: Pull taskfile repo'
git -C "$HOME/taskfile.build" pull
fi
}
if [[ -d "$HOME/taskfile.build/bin" ]]; then
for file in "$HOME/taskfile.build/bin/"*; do
if [[ -f "$file" ]]; then
source "$file"
fi
done
fi
# Help commands
info() {
set-addons-path
echo "OS Release: $OS_RELEASE"
echo "Odoo Revision: $ODOO_REVISION"
echo "Odoo Version: $ODOO_VERSION"
echo "Odoo Port: $ODOO_PORT"
echo "Postgres Port: $POSTGRES_PORT"
echo "Odoo Language: $ODOO_LANGUAGE"
echo "Odoo Param: $ODOO_PARAM"
echo "Container Registry: $CONTAINER_REGISTRY"
echo "Container Config: $CONTAINER_CONFIG"
echo "Addons Path: $ADDONS_PATH"
}
version() {
uv --version
activate-venv
python -V
wkhtmltopdf -V
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
docker -v
docker compose version
else
podman --version
podman-compose --version
fi
}
generate-admin-password() {
if [[ -z "$1" ]]; then echo '\$1/pass 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
# Add included addons
# local includes="addons/thirdparty"
# for include in $includes; do
# git_folders_path="$include,$git_folders_path"
# done
# Filter valid directories
local valid_paths=""
while IFS= read -r addon; do
addon=$(echo "$addon" | xargs)
[[ -z "$addon" ]] && continue
if [[ -d "$addon" ]]; then
valid_paths="${valid_paths:+$valid_paths,}$addon"
else
echo "Set Addons Path: Path '$addon' does not exist." >&2
fi
done < <(echo "$git_folders_path" | tr ',' '\n')
git_folders_path="$valid_paths"
# Convert to source paths (add /addons where needed)
local addons_path="$(echo "$git_folders_path" | sed 's|,odoo|,odoo/addons|g')"
# Append paths from config env var
if [[ -n "$ODOO_ADDONS_PATH" ]]; then
addons_path="${ODOO_ADDONS_PATH},${addons_path}"
fi
# Export the addons path
export ADDONS_PATH="$addons_path"
}
load-ssh-key() {
if [[ -n "$GIT_SSH_PRIVATE_KEY" ]]; then
echo 'Setup SSH key from env var.'
local decoded_git_ssh_private_key=$(echo -e "$GIT_SSH_PRIVATE_KEY" | base64 -d)
mkdir -p ~/.ssh
echo "$decoded_git_ssh_private_key" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519 || (echo 'Dumping ~/.ssh/id_ed25519 content:' && cat ~/.ssh/id_ed25519)
else
echo 'The SSH key env var is empty.'
exit 1
fi
}
# Docs commands
lint() {
echo "Run pre-commit in $PWD"
pre-commit run
echo "Run snippet linting"
lint-snippets
}
update-snippets-doc() {
./bin/update-snippets-doc
}
update-revisions-doc() {
echo 'Update revisions doc file.'
rm -f "revisions.md"
echo -e "# Odoo Revisions \n\
A Odoo revision is a snapshot of git references of the Odoo source and modules at a specific date.\n\
For each major release there are multiple revisions.\n" > "revisions.md"
declare -A revisions
local path_url="https://github.com/${USER_ORG}/Odoo-Build/tree/main"
for file in $(ls revisions/ | sort -rV); do
local revision="$file"
local version=$(echo "$revision" | cut -d'.' -f1-2)
if [[ -z "${revisions[$version]}" ]]; then
revisions[$version]="## $version"
fi
revisions[$version]+=$'\n\n'
revisions[$version]+="#### $revision"
revisions[$version]+=$'\n\n```bash\n'
revisions[$version]+=$(cat "revisions/$file")
revisions[$version]+=$'\n```'
done
for revision in $(echo "${!revisions[@]}" | tr ' ' '\n' | sort -rV); do
echo -e "${revisions[$revision]}\n" >> "revisions.md"
done
}
update-modules-doc() {
echo 'Update module doc file.'
./bin/update-modules-doc
}
update-docs() {
update-modules-doc
update-revisions-doc
update-snippets-doc
}
update-module-license() {
if [[ -z "$1" ]]; then echo '\$1/grep is empty.'; exit; fi
local git_folders=$(list-module "$1")
for git_folder in $git_folders; do
echo "Update license file for $git_folder."
cp templates/module/LICENSE "$git_folder/"
done
}
generate-repo-docs(){
if [[ -z "$1" ]]; then
echo '\$1/path 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/path 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/path is empty.'
else
# Get version of module
local version=$(python -c "import ast; print(ast.literal_eval(open('$1/__manifest__.py').read())['version'])")
# Set default version
[[ -z "$version" ]] && version=0.0
local count_dots=$(echo "$version" | grep -o "\." | wc -l)
# Check if oca version or enterprise version
if [[ $count_dots == 2 ]]; then
version="$ODOO_VERSION.$version"
fi
if [[ $count_dots == 1 ]]; then
version="$ODOO_VERSION.$version"
fi
echo "$version"
fi
}
get-module-checksum() {
if [[ -z "$1" ]]; then
echo '\$1/path is empty.'
else
bin/get-module-checksum "$1"
fi
}
list-modules() {
if [[ -z "$1" ]]; then echo '\$1/path is empty.'; exit; fi
local modules=$(get-modules "$1" dirname)
echo "$modules" | tr ',' '\n'
}
# Container commands
archive-docker-tags() {
export CONTAINER_CONFIG
python bin/archive-docker-tags
}
template-compose() {
echo 'Template compose.yml'
# Load and export vars defined in compose template
for var in $(grep -oE '\$\{?[A-Z_][A-Z0-9_]*\}?' compose.yml.template); do
# Strip $, ${, }
var=${var#\$\{} # Remove leading ${
var=${var%\}} # Remove trailing }
var=${var#\$} # Remove leading $
export "$var"
done
# Add volume mounts
VOLUME_MOUNTS=""
if [[ -d "./tmp/postgres-odoo" ]]; then
VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./tmp/postgres-odoo:/mnt/postgres-odoo"$'\n'
fi
if [[ -d "./addons" ]]; then
VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./addons:/mnt/addons"$'\n'
fi
if [[ -d "./oca" ]]; then
VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./oca:/mnt/oca"$'\n'
fi
if [[ -d "./enterprise" ]]; then
VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./enterprise:/mnt/enterprise"$'\n'
fi
if [[ -d "./themes" ]]; then
VOLUME_MOUNTS="${VOLUME_MOUNTS} - ./themes:/mnt/themes"$'\n'
fi
VOLUME_MOUNTS="${VOLUME_MOUNTS%$'\n'}"
export VOLUME_MOUNTS
envsubst < "compose.yml.template" > "compose.yml"
}
container-ps() {
$CONTAINER_ENGINE ps
}
build-odoo() {
odoo_date=$(echo "$ODOO_REVISION" | cut -d'.' -f3-)
source "revisions/$ODOO_REVISION"
manifest_file="images/odoo/Dockerfile"
container_tag="odoo:$ODOO_VERSION"
container_revision_tag="odoo:$ODOO_REVISION"
case "$ODOO_VERSION" in
"15.0")
local python_version="3.9"
PLATFORM="linux/amd64"
;;
"16.0")
local python_version="3.11"
;;
"17.0")
local python_version="3.11"
;;
"18.0")
local python_version="3.12"
;;
"19.0")
local python_version="3.12"
;;
esac
local docker_output="--load"
if [[ -n "$1" ]]; then
docker_output="$1"
fi
echo "Python Version: $python_version"
echo "Platform: $PLATFORM"
echo "Enterprise Ref: $ODOO_ENTERPRISE_REF"
echo "Docker Output: $docker_output"
echo "Remove container image with tag ${CONTAINER_REGISTRY}/${container_revision_tag}"
docker image rm -f "${CONTAINER_REGISTRY}/${container_revision_tag}"
if ! docker buildx inspect odoo_builder >/dev/null 2>&1; then
docker buildx create --name odoo_builder --use
docker buildx inspect --bootstrap
else
docker buildx use odoo_builder
fi
echo "Run Docker build ${CONTAINER_REGISTRY}/${container_revision_tag} for ${PLATFORM}"
SOURCE_DATE_EPOCH=$(git -C odoo log -1 --pretty=%ct) docker buildx build --platform "$PLATFORM" . \
--file "$manifest_file" \
--build-arg PYTHON_VERSION="$python_version" \
--build-arg ODOO_VERSION="$ODOO_VERSION" \
--build-arg ODOO_DATE="$odoo_date" \
--build-arg ODOO_ENTERPRISE_REF="$ODOO_ENTERPRISE_REF" \
--tag "${CONTAINER_REGISTRY}/${container_tag}" \
--tag "${CONTAINER_REGISTRY}/${container_revision_tag}" \
"$docker_output"
}
build-odoo-cli() {
manifest_file="images/odoo-cli/Dockerfile"
container_tag="odoo-cli:latest"
local docker_output="--load"
if [[ -n "$1" ]]; then
docker_output="$1"
fi
echo "Platform: $PLATFORM"
echo "Docker Output: $docker_output"
echo "Remove container image with tag ${CONTAINER_REGISTRY}/${container_tag}"
docker image rm -f "${CONTAINER_REGISTRY}/${container_tag}"
if ! docker buildx inspect odoo_builder >/dev/null 2>&1; then
docker buildx create --name odoo_builder --use
docker buildx inspect --bootstrap
else
docker buildx use odoo_builder
fi
echo "Run Docker build ${CONTAINER_REGISTRY}/${container_tag} for ${PLATFORM}"
SOURCE_DATE_EPOCH=$(git -C odoo log -1 --pretty=%ct) docker buildx build --platform "$PLATFORM" . \
--file "$manifest_file" \
--tag "${CONTAINER_REGISTRY}/${container_tag}" \
"$docker_output"
}
build-odoo-mailgate() {
manifest_file="images/odoo-mailgate/Dockerfile"
container_tag="odoo-mailgate:latest"
local docker_output="--load"
if [[ -n "$1" ]]; then
docker_output="$1"
fi
echo "Platform: $PLATFORM"
echo "Docker Output: $docker_output"
echo "Remove container image with tag ${CONTAINER_REGISTRY}/${container_tag}"
docker image rm -f "${CONTAINER_REGISTRY}/${container_tag}"
if ! docker buildx inspect odoo_builder >/dev/null 2>&1; then
docker buildx create --name odoo_builder --use
docker buildx inspect --bootstrap
else
docker buildx use odoo_builder
fi
echo "Run Docker build ${CONTAINER_REGISTRY}/${container_tag} for ${PLATFORM}"
SOURCE_DATE_EPOCH=$(git -C odoo log -1 --pretty=%ct) docker buildx build --platform "$PLATFORM" . \
--file "$manifest_file" \
--tag "${CONTAINER_REGISTRY}/${container_tag}" \
"$docker_output"
}
build-odoo-upgrade() {
local manifest_file="images/odoo-upgrade/Dockerfile"
local postgres_versions="14 15 16 17"
local docker_output="--load"
if [[ -n "$1" ]]; then
docker_output="$1"
fi
if ! docker buildx inspect odoo_builder >/dev/null 2>&1; then
docker buildx create --name odoo_builder --use
docker buildx inspect --bootstrap
else
docker buildx use odoo_builder
fi
for postgres_version in $postgres_versions; do
container_tag="odoo-upgrade:$postgres_version"
echo "Posgres Version: $postgres_version"
echo "Platform: $PLATFORM"
echo "Docker Output: $docker_output"
echo "Removing existing image: $full_tag"
docker image rm -f "$full_tag" 2>/dev/null || true
echo "Running Docker build for $full_tag on platform $PLATFORM"
SOURCE_DATE_EPOCH=$(git -C odoo log -1 --pretty=%ct) \
docker buildx build --platform "$PLATFORM" . \
--file "$manifest_file" \
--build-arg POSTGRES_VERSION="$postgres_version" \
--tag "${CONTAINER_REGISTRY}/${container_tag}" \
"$docker_output"
if [[ $? -ne 0 ]]; then
echo "Build failed for PostgreSQL $postgres_version"
return 1
fi
echo "Build succeeded for PostgreSQL $postgres_version"
done
}
load-latest-revision() {
if [[ -n "$1" ]]; then
ODOO_VERSION="$1"
fi
ODOO_REVISION=$(list-revision | grep "$ODOO_VERSION" | tail -n1)
load-revision "$ODOO_REVISION"
}
checkout-latest-revision() {
if [[ -n "$1" ]]; then
ODOO_VERSION="$1"
fi
ODOO_REVISION=$(list-revision | grep "$ODOO_VERSION" | tail -n1)
checkout-revision "$ODOO_REVISION"
check-version
}
container-login() {
if [[ -n "$1" ]]; then
CONTAINER_REGISTRY_USERNAME="$1"
fi
if [[ -n "$2" ]]; then
CONTAINER_REGISTRY_PASSWORD="$2"
fi
echo "Docker login with username $CONTAINER_REGISTRY_USERNAME."
echo "$CONTAINER_REGISTRY_PASSWORD" | docker --config "$CONTAINER_CONFIG" login --username "$CONTAINER_REGISTRY_USERNAME" --password-stdin
echo "$CONTAINER_REGISTRY_PASSWORD" | docker --config "$CONTAINER_CONFIG" login dhi.io --username "$CONTAINER_REGISTRY_USERNAME" --password-stdin
}
login-podman() {
if [[ -n "$1" ]]; then
CONTAINER_REGISTRY_USERNAME="$1"
fi
if [[ -n "$2" ]]; then
CONTAINER_REGISTRY_PASSWORD="$2"
fi
echo "Podman login with username $CONTAINER_REGISTRY_USERNAME."
podman login --username "$CONTAINER_REGISTRY_USERNAME" --password "$CONTAINER_REGISTRY_PASSWORD" docker.io
}
update-docker-hub-readme() {
bin/update-docker-hub-readme
}
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-script() {
# Test the scripts of the Docker images without starting the enviroment.
set -e
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
if [[ "$1" == "images/odoo" ]]; then
generate-ssh-keys
export GIT_SSH_PRIVATE_KEY
export GIT_SSH_PUBLIC_KEY
export PATH=./images/odoo/bin/:$PATH
export LOCAL_PATH="./tmp"
mkdir -p "$LOCAL_PATH"
echo -e "\033[38;5;214mTEST\033[0m: Run git clone addons"
local server_tools_git_ref=$(curl -s "https://api.github.com/repos/OCA/server-tools/branches/$ODOO_VERSION" | grep '"sha":' | head -n 1 | awk -F '"' '{print $4}')
export ADDONS_GIT_REPOS="https://github.com/OCA/server-tools.git#${ODOO_VERSION}#${server_tools_git_ref}"
# This will remove all SSH keys at the end
./images/odoo/bin/clone-git-addons
rm -rf "$LOCAL_PATH/github.com" # Cleanup for next run
echo -e "\033[38;5;214mTEST\033[0m: Install python package."
export PYTHON_INSTALL="prometheus-client,pyjwt"
./images/odoo/bin/install-python-packages
echo -e "\033[38;5;214mTEST\033[0m: Sync python project."
cat > tmp/pyproject.toml << 'EOF'
[project]
name = "odoo-build-project"
version = "0.1.0"
description = "Test project for uv sync."
dependencies = [
"requests>=2.28.0",
"click>=8.0.0",
]
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.uv]
dev-dependencies = [
"pytest>=7.0.0",
"black>=23.0.0",
]
EOF
./images/odoo/bin/sync-python-project
uv pip list
echo -e "\033[38;5;214mTEST\033[0m: Parse url."
git_proto=$(./images/odoo/bin/parse-url "git@github.com:OCA/server-tools.git" proto)
[[ "$git_proto" == "ssh://" ]] && log-success "Test success."
fi
if [[ "$1" == "images/odoo-cli" ]]; then
echo -e "\033[38;5;214mTEST\033[0m: Show help of odoocli."
docker_odoocli() {
docker run -it \
-e ODOO_URL="$ODOO_URL" -e DB_NAME="$DB_NAME" -e ODOO_USERNAME="$ODOO_USERNAME" -e ODOO_PASSWORD="$ODOO_PASSWORD" \
mintsystem/odoocli "$@"
}
docker_odoocli --help
fi
if [[ "$1" == "images/odoo-upgrade" ]]; then
echo -e "\033[38;5;214mTEST\033[0m: Show help of odoo-upgrade."
docker run -it mintsystem/odoo-upgrade --help
fi
}
upload-module() {
if [[ -z "$1" ]]; then
echo '$1/env is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/path 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
list-services() {
echo "db"
echo "mailgate"
echo "mailpit"
echo "odoo"
echo "odoo-nginx"
echo "source"
}
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
echo "Source .env.odoo"
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" =~ "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 "http://localhost:8025"
$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 "http://localhost:$port/web?debug=1&db=$database"
odoo --database="$database" --config="$ODOO_RC" \
--http-port="$port" --addons-path="$ADDONS_PATH" --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 "http://localhost:8069"
$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 "http://localhost:8080"
$COMPOSE_COMMAND up -d odoo
fi
}
debug() {
if [[ -z "$1" ]]; then
echo '$1/name is empty.'
exit
fi
local database="$2"
if [[ -z "$database" ]]; then
database="$DB_NAME"
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/name is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/cmd is empty.'
exit
fi
template-compose
echo "Run '$1' container with command '$2'."
$COMPOSE_COMMAND run --rm "$1" "$2" "$3"
}
init-odoo-jupyter() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
./bin/init-odoo-jupyter "$1"
}
start-odoo-jupyter() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local database="$2"
if [[ -z "$database" ]]; then
database="$DB_NAME"
fi
local token="odoo"
./bin/start-odoo-jupyter "$1" "$database" "$token"
}
exec-odoo-jupyter() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local database="$2"
if [[ -z "$database" ]]; then
database="$DB_NAME"
fi
./bin/exec-odoo-jupyter "$1" "$database"
}
exec() {
if [[ -z "$1" ]]; then
echo '$1/container 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/pid 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/name is empty.'
exit
fi
local database="$2"
if [[ -z "$database" ]]; then
database="$DB_NAME"
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" =~ "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
docker volume rm odoo-build_odoo-data
git -C oca/partner-contact clean -df
git -C oca/partner-contact checkout .
else
version
info
set -e
echo -e "\033[38;5;214mTEST\033[0m: Clone module repo with https"
(git clone -b "$ODOO_VERSION" https://github.com/OCA/partner-contact.git oca/partner-contact) || true
echo -e "\033[38;5;214mTEST\033[0m: Start container environment"
export BROWSER_OPEN=false
start db,mail
echo -e "\033[38;5;214mTEST\033[0m: Clone Odoo addons"
generate-ssh-keys
export GIT_SSH_PRIVATE_KEY
export GIT_SSH_PUBLIC_KEY
export ADDONS_GIT_REPOS="https://github.com/OCA/server-env.git#${ODOO_VERSION},https://github.com/OCA/server-tools.git#${ODOO_VERSION},https://github.com/Mint-System/Odoo-Apps-Server-Tools.git#${ODOO_VERSION}"
run odoo clone-git-addons
echo -e "\033[38;5;214mTEST\033[0m: Init Odoo database"
export ODOO_ADDONS_PATH="/mnt/oca/partner-contact"
export DB_NAME=test
export WITHOUT_DEMO=False
export ODOO_INIT_LOGIN=test
export ODOO_INIT_PASSWORD=test
export ODOO_INIT_LANG=de_CH
export ODOO_INIT_ADDONS=server_environment_ir_config_parameter,module_auto_update,prometheus_exporter,mail_server_filter,contacts,web_enterprise
export PYTHON_INSTALL=prometheus-client,manifestoo
run odoo init-db
echo -e "\033[38;5;214mTEST\033[0m: Start Odoo container"
export ODOO_MAIL_SMTP_HOST=mail
export ODOO_MAIL_SMTP_PORT=1025
export ODOO_MAIL_SMTP_ENCRYPTION=none
export SERVER_WIDE_MODULES=module_change_auto_install
export MODULE_AUTO_INSTALL_DISABLED=odoo_test_xmlrunner
export AUTO_UPDATE_TRANSLATIONS=True
export AUTO_UPDATE_MODULES_LIST=True
export MAIL_DEFAULT_FROM="test"
export BROWSER_OPEN=false
export TEST_ADDONS_DIR=/mnt/oca/partner-contact
export TEST_INCLUDE=partner_firstname
start odoo
TIMEOUT=60
ELAPSED=0
until curl --silent --fail "http://localhost:8069/web/login" &> /dev/null; do
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "Odoo did not start within $TIMEOUT seconds."
docker logs odoo
exit 1
fi
echo 'Waiting for Odoo to be ready...'
sleep 3
ELAPSED=$((ELAPSED + 3))
done
echo -e "\033[38;5;214mTEST\033[0m: Setup requirements.txt file"
exec odoo bash -c "echo 'click>=8.0.0' > /var/lib/odoo/requirements.txt"
exec odoo install-python-packages
# FIXME: Cannot use exec
click_path=$(docker exec odoo python3 -c 'import click; print(click.__file__)')
if echo "$click_path" | grep -q "/opt/odoo/venv/"; then
log-success "click is installed in persisted venv."
else
log-error "click is NOT in /opt/odoo/venv/ — found at: $click_path"
exit 1
fi
echo -e "\033[38;5;214mTEST\033[0m: Init repos.yml file"
cat << EOF > tmp/repos.yml
github.com/Mint-System/Odoo-Apps-Partner-Contact:
remotes:
mint_system: https://github.com/Mint-System/Odoo-Apps-Partner-Contact.git
merges:
- mint_system ${ODOO_VERSION}
target: mint_system ${ODOO_VERSION}
EOF
docker cp tmp/repos.yml odoo:/var/lib/odoo/git/repos.yml
exec odoo aggregate-git-repos
echo -e "\033[38;5;214mTEST\033[0m: Init OCA module"
exec odoo init-module partner_firstname
echo -e "\033[38;5;214mTEST\033[0m: Setup and check RPC credentials"
export ODOO_URL="http://localhost:8069"
export ODOO_DATABASE=test
export ODOO_USERNAME=test
export ODOO_PASSWORD=test
if ! show-dotenv test; then
create-dotenv test odoo
fi
show-dotenv test
test-xmlrpc test
echo -e "\033[38;5;214mTEST\033[0m: Install, update, disable and remove snippet"
install-snippet test snippets/mint_system.base.view_partner_form.show_type.xml
update-snippet test snippets/mint_system.base.view_partner_form.show_type.xml
disable-snippet test snippets/mint_system.base.view_partner_form.show_type.xml
remove-snippet test snippets/mint_system.base.view_partner_form.show_type.xml
echo -e "\033[38;5;214mTEST\033[0m: Check module auto update"
sed -i 's/First name/Firstname/g' oca/partner-contact/partner_firstname/models/res_partner.py
exec odoo update-modules
# FIXME: Check if module is auto updated
# docker logs odoo 2>&1 | grep "Run click-odoo-update."
echo -e "\033[38;5;214mTEST\033[0m: Check translations auto update"
sed -i 's/Nachname/Familienname/g' oca/partner-contact/partner_firstname/i18n/de.po
restart odoo
# FIXME: Check if translation is updated
# docker logs odoo 2>&1 | grep "Updating translations for all active languages and installed modules."
# field_description=$(shell test "print(env.ref('partner_firstname.field_res_partner__lastname').field_description)")
# if [[ "$user_count" != "Familienname" ]]; then
# log-error "Expected 'Familienname' field description, but got $field_description"
# exit 1
# fi
# 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}.9.0.0
cat << EOF > oca/partner-contact/partner_firstname/migrations/${ODOO_VERSION}.9.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 += "x"
_logger.info("Updated %s partners", len(partners))
EOF
sed -i "/\"version\":/c\ \"version\": \"${ODOO_VERSION}.9.0.0\"," oca/partner-contact/partner_firstname/__manifest__.py
echo "Execute the module upgrade."
exec odoo update-module partner_firstname
# partner_count=$(shell test "print(env['res.partner'].search_count([('name','=','Mitchell Adminx')]))")
# if [[ "$user_count" != "1" ]]; then
# log-error "Expected 1 partner, but got $partner_count"
# exit 1
# fi
# FIXME: Check if module is migrated
# docker logs odoo 2>&1 | grep "module partner_firstname: Running migration"
# echo -e "\033[38;5;214mTEST\033[0m: Drop the test database"
# drop-db test
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 --database="$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 --database="$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/db 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/db 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 --database="$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"
}
update-module-list() {
if [[ -z "$1" ]]; then
echo '$1/db 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/db 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 --database="$database" --config="$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http
}
visualize-dependencies() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
mkdir -p tmp
./bin/visualize-dependencies "$1"
open-with-os tmp/odoo-module-dependencies.html
cat tmp/odoo-module-dependencies.mmd
}
# Setup commands
install-requirements() {
if [[ -z "$1" ]]; then
echo '$1/db 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/db 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 --database="$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/db 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/path 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/"
if [[ "$ODOO_VERSION" == "19.0" ]]; then
odoo --addons-path="$ADDONS_PATH" i18n export "$module_name" --languages "$language" -d "$database" -c "$ODOO_RC"
else
odoo --database="$database" --config="$ODOO_RC" --addons-path="$ADDONS_PATH" --modules "$module_name" -l "$language" --i18n-export "$module_path/i18n/$language.po"
fi
# 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/path is empty.'
exit
fi
local module=$(basename "$1")
set-addons-path
echo "Testing module $module..."
odoo -d "$DB_NAME" -i "$module" --config="$ODOO_RC" --addons-path="$ADDONS_PATH" --stop-after-init --no-http --test-enable
}
test-modules() {
if [[ -z "$1" ]]; then
echo '$1/path 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/path 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"
}
uninstall-module() {
if [[ -z "$1" ]]; then
echo '$1/db 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/path is empty.'
exit
fi
local module_path="$1"
local module_name=$(basename "$module_path")
local parent_dir=$(dirname "$module_path")
echo "Scaffolding module $module_name in ${parent_dir}..."
odoo scaffold "$module_name" "$parent_dir" -t templates/module
echo 'Prompt for module manifest values...'
export MODULE_TITLE=$(echo "$module_name" | sed 's/[^_]\+/\L\u&/g' | sed 's/_/ /g')
read -p "Summary: " MODULE_SUMMARY
export MODULE_SUMMARY
while true; do
read -p "License (AGPL-3/OPL-1): " MODULE_LICENSE
[[ "$MODULE_LICENSE" == "AGPL-3" || "$MODULE_LICENSE" == "OPL-1" ]] && break
echo "Invalid. Use AGPL-3 or OPL-1."
done
export MODULE_LICENSE
read -p "Depends (comma-separated and quoted): " MODULE_DEPENDS
export MODULE_DEPENDS
while true; do
read -p "Data? (y/n): " DATA_RESPONSE
if [[ "$DATA_RESPONSE" == "y" ]]; then
export MODULE_DATA_KEY='"data": ["data/data.xml"],'
mkdir -p "$module_path/data"
cp templates/data.xml "$module_path/data/data.xml"
break
elif [[ "$DATA_RESPONSE" == "n" ]]; then
export MODULE_DATA_KEY='"data": [],'
break
else
echo "Answer y or n."
fi
done
while true; do
read -p "Demo? (y/n): " DEMO_RESPONSE
if [[ "$DEMO_RESPONSE" == "y" ]]; then
export MODULE_DEMO_KEY='"demo": ["demo/demo.xml"],'
mkdir -p "$module_path/demo"
cp templates/demo.xml "$module_path/demo/demo.xml"
break
elif [[ "$DEMO_RESPONSE" == "n" ]]; then
export MODULE_DEMO_KEY=""
break
else
echo "Answer y or n."
fi
done
while true; do
read -p "Test instructions? (y/n): " TEST_RESPONSE
if [[ "$TEST_RESPONSE" == "y" ]]; then
mkdir -p "$module_path/tests"
touch "$module_path/tests/TEST_INSTRUCTIONS.rst"
break
elif [[ "$TEST_RESPONSE" == "n" ]]; then
break
else
echo "Answer y or n."
fi
done
while true; do
read -p "Security? (y/n): " TEST_RESPONSE
if [[ "$TEST_RESPONSE" == "y" ]]; then
export MODULE_DATA_KEY='"data": ["security/security.xml"],'
mkdir -p "$module_path/security"
cp templates/security.xml "$module_path/security/security.xml"
break
elif [[ "$TEST_RESPONSE" == "n" ]]; then
export MODULE_DATA_KEY='"data": [],'
break
else
echo "Answer y or n."
fi
done
export ODOO_VERSION
envsubst < templates/module/__manifest__.py > "$module_path/__manifest__.py"
echo "Created the module $module_path"
generate-module-docs "$module_path"
}
generate-module-docs() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local module_path="$1"
local manifest_path="$module_path/__manifest__.py"
echo 'Setup readme folder'
mkdir -p "$module_path/readme"
cp -n -r "templates/module/readme" "$module_path"
echo 'Update DESCRIPTION.rst'
export MODULE_SUMMARY=$(python3 -c "import ast; d=ast.literal_eval(open('$manifest_path').read()); print(d.get('summary', '').replace('\n', ' ').strip())")
envsubst < templates/module/readme/DESCRIPTION.md > "$module_path/readme/DESCRIPTION.md"
echo 'Update CONTRIBUTORS.rst'
export GIT_USER_NAME="$(git config user.name)"
export GIT_USER_EMAIL="$(git config user.email)"
envsubst < templates/module/readme/CONTRIBUTORS.md > "$module_path/readme/CONTRIBUTORS.md"
echo 'Copy icon.png'
cp templates/module/static/description/icon.png "$module_path/static/description/icon.png"
echo 'Copy LICENSE'
cp templates/module/LICENSE "$module_path/"
echo 'Touch __init__.py'
touch "$module_path/__init__.py"
local repo_url=$(git -C "$(dirname $module_path)" remote get-url origin)
local repo_name=$(basename "${repo_url%.git}")
echo "Generate OCA README.rst"
oca-gen-addon-readme --org-name=$USER_ORG --repo-name=$repo_name --branch=$ODOO_VERSION --addon-dir=$module_path
}
generate-module-migration() {
if [[ -z "$1" ]]; then
echo '$1/path 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/path is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/model is empty.'
exit
fi
local module_name=$(basename "$1")
local parent_dir=$(dirname "$1")
local model_dot_name="$2"
local model_name=$(echo "$model_dot_name" | tr '.' '_')
local access_file="$parent_dir/$module_name/security/ir.model.access.csv"
mkdir -p "$parent_dir/$module_name/security"
if [[ ! -f "$access_file" ]]; then
echo "Init security file: $access_file"
echo "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink" > "$access_file"
fi
# id
# name
# model_id:id
# group_id:id
# perm_read
# perm_write
# perm_create
# perm_unlink
echo "Add model access to $access_file"
echo "access_${model_name}_user,access.$model_dot_name.user,model_${model_name},base.group_user,1,0,0,0" >> "$access_file"
echo "access_${model_name}_manager,access.$model_dot_name.manager,model_${model_name},base.group_erp_manager,1,1,1,1" >> "$access_file"
echo "Update file $module_name/__manifest__.py with:"
echo -e "\t\"security/ir.model.access.csv\","
}
create-repo() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local repo_path="$1"
local repo_folder=$(basename "$repo_path")
local repo_name=$(echo "$repo_folder" | sed 's/_/-/g' | sed 's/\b\(.\)/\u\1/g')
echo "Create folder $repo_path."
mkdir -p "$repo_path"
generate-repo-docs "$repo_path"
echo "Init git with branch $ODOO_VERSION"
git -C "$repo_path" init --initial-branch "$ODOO_VERSION"
local git_url="git@github.com:${USER_ORG}/Odoo-Apps-$repo_name"
echo "Add git remote: $git_url "
git -C "$repo_path" remote add origin "$git_url"
echo -e "Use this command to register as git folder:\n"
echo -e "\ttask add-git-folder $git_url $ODOO_VERSION $repo_path\n"
}
init-repo-branch() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local repo_path="$1"
local repo_name=$(basename "$repo_path")
echo "Create new branch $ODOO_VERSION in $repo_path"
git -C "$repo_path" checkout --orphan="$ODOO_VERSION"
echo "Remove all files from $repo_path"
rm -rf "$repo_path/"*
generate-repo-docs "$repo_path"
local git_url="git@github.com:${USER_ORG}/Odoo-Apps-$repo_name"
echo -e "Use this command to register as git folder:\n"
echo -e "\ttask add-git-folder $git_url $ODOO_VERSION $repo_path\n"
}
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/path is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/model is empty.'
exit
fi
local module_path="$1"
echo 'Substitute model template'
export-module-vars "$1" "$2"
envsubst < templates/model.py > "$module_path/models/$MODEL_NAME.py"
echo "from . import $MODEL_NAME" >> "$module_path/models/__init__.py"
echo "File "$module_path/models/$MODEL_NAME.py created.
}
generate-module-wizard() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/model is empty.'
exit
fi
local module_path="$1"
echo 'Create wizard folder and init file'
mkdir -p "$module_path/wizard"
touch "$module_path/wizard/__init__.py"
echo 'Substitute wizard template'
export-module-vars "$1" "$2"
envsubst < templates/wizard.py > "$module_path/wizard/$MODEL_NAME.py"
echo "from . import $MODEL_NAME" >> "$module_path/wizard/__init__.py"
echo "File "$module_path/wizard/$MODEL_NAME.py created.
}
generate-module-translation() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local module_path="$1"
local module_name=$(basename "$1")
echo "Translate module $module_name."
translate-module "$module_path"
echo "Translate the de.po with llm..."
task update-with-llm "$module_path/i18n/de.po" "Add missing translations."
}
generate-module-inherit-model() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/model is empty.'
exit
fi
local module_path="$1"
local module_name=$(basename "$1")
echo 'Substitute 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/path is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/model is empty.'
exit
fi
local module_path="$1"
local module_name=$(basename "$1")
echo 'Substitute view template'
mkdir -p "$module_path/views"
export-module-vars "$1" "$2"
envsubst < templates/view.xml > "$module_path/views/${MODEL_NAME}.xml"
echo "Update file $module_name/__manifest__.py with:"
echo -e "\t\"views/${MODEL_NAME}.xml\","
}
generate-module-inherit-view() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/ref is empty.'
exit
fi
if [[ -z "$3" ]]; then
echo '$3/model is empty.'
exit
fi
local module_path="$1"
echo 'Substitute inherit 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/inherit.xml > "$module_path/views/${MODEL_NAME}.xml"
echo "Update file $MODULE_NAME/__manifest__.py with:"
echo -e "\t\"views/${MODEL_NAME}.xml\","
}
generate-module-report() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/model is empty.'
exit
fi
local module_path="$1"
local module_name=$(basename "$1")
echo 'Substitute view template'
mkdir -p "$module_path/report"
export-module-vars "$1" "$2"
envsubst < templates/template.xml > "$module_path/report/${MODEL_NAME}.xml"
echo "Update file $module_name/__manifest__.py with:"
echo -e "\t\"report/${MODEL_NAME}.xml\","
}
release-module() {
if [[ -z "$1" ]]; then
echo '$1/path 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/path 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
}
register-module-repos() {
local version="${1:-$ODOO_VERSION}"
local path="${2:-addons}"
if [[ -z "$version" ]]; then
echo 'Error: ODOO_VERSION not set and no version provided.'
exit 1
fi
echo "Registering module repos for Odoo version: $version"
echo "Path: $path"
echo ""
./bin/register-module-repos "$path" "$version"
}
# Upgrade commands
prepare-migration-brach() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/module is empty.'
exit
fi
if [[ -z "$3" ]]; then
echo '$3/version is empty.'
exit
fi
local repo="$1"
local module="$2"
local version="$3"
echo "Create migration branch for $module module in $repo for version $ODOO_VERSION"
git -C "$repo" checkout -b "$ODOO_VERSION-mig-$module" "origin/$ODOO_VERSION"
echo "Checkout $module from $version"
git -C "$repo" checkout "$version" "$module"
echo "Create patch file from version $version to $ODOO_VERSION"
git format-patch --keep-subject --stdout origin/$ODOO_VERSION..origin/$version -- $module | git am -3 --keep
echo "Run pre-commit"
cd "$repo"
pre-commit run -a
echo "Commit the changes"
git add --all
git commit -m "[IMP] $module: pre-commit auto fixes" --no-verify
echo "Add git remote for OCA"
git remote add oca $(git remote get-url origin)
echo "Add git remote for $USER_ORG"
local user_org="${USER_ORG,,}"
local repo_name=$(echo "$repo" | sed 's/_/-/g' | sed 's/\b\(.\)/\u\1/g')
git remote add "$user_org" "https://github.com/${USER_ORG}/$repo_name"
}
migrate-module() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local module_path="$1"
local source_odoo_version=$(get-module-version $module_path | cut -d'.' -f1,2)
local module=$(basename "$module_path")
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
}
backport-module() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local module_path="$1"
local source_odoo_version=$(get-module-version $module_path | cut -d'.' -f1,2)
local module=$(basename "$module_path")
echo "Replace manifest version $source_odoo_version with $ODOO_VERSION."
sed -i -E "s/$source_odoo_version/$ODOO_VERSION/g" "$module_path/__manifest__.py"
}
run-openupgrade() {
if [[ -z "$1" ]]; then
echo '$1/database is empty.'
exit
fi
database="$1"
echo "Run OpenUpgrade for database '$database'..."
set-addons-path
template-odoo-rc
odoo --database="$database" --config="$ODOO_RC" --addons-path="$ADDONS_PATH" --update all --stop-after-init --load=base,web,openupgrade_framework
}
# Snippet commands
reset-views() {
if [[ -z "$1" ]]; then
echo '$1/db 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/xml-id 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/env is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/path is empty.'
exit
fi
# Generate view name from filename
local file=$(basename "$2")
local name="${file%.xml}"
local inherit_id=$(echo "$name" | cut -d. -f 2,3 )
local module_name=$(echo "$inherit_id" | cut -d. -f 1 )
local xml_id=$(echo "$inherit_id" | cut -d. -f 2 )
local type=$(echo 'cat //data/@type' | xmllint --shell "$2" | awk -F'[="]' '!/>/{print $(NF-1)}')
local model=$(echo 'cat //data/@model' | xmllint --shell "$2" | awk -F'[="]' '!/>/{print $(NF-1)}')
local priority=$(echo 'cat //data/@priority' | xmllint --shell "$2" | awk -F'[="]' '!/>/{print $(NF-1)}')
# Set default values
: ${type:='qweb'}
load-dotenv "$1"
echo "Search for view with name $module_name.$xml_id"
inherit_id=$(images/odoo-cli/bin/odoocli --method "read" --model "ir.model.data" --field "res_id" --domain "[('name', '=', '$xml_id'), ('module', '=', '$module_name')]")
echo "Found view id $inherit_id"
echo "Apply view $name"
images/odoo-cli/bin/odoocli --method 'create' --model 'ir.ui.view' --value "{
'name': '$name',
'type': '$type',
'model': '$model',
'mode': 'extension',
'priority': $priority,
'inherit_id': $inherit_id,
'arch_base': '''$(cat "$2")'''
}"
}
update-snippet() {
if [[ -z "$1" ]]; then
echo '$1/env is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/path is empty.'
exit
fi
# Generate view name from filename
local file=$(basename "$2")
local name="${file%.xml}"
load-dotenv "$1"
echo "Update view with name $name"
images/odoo-cli/bin/odoocli --method 'write' --model 'ir.ui.view' --include-archived --domain "[('name', '=', '$name')]" --field 'arch_base' --value "'''$(cat "$2")'''"
enable-snippet "$1" "$2"
}
disable-snippet() {
if [[ -z "$1" ]]; then
echo '$1/env is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/path is empty.'
exit
fi
# Get name from path
# path: snippets/mint_system.base.view_partner_tree.show_type.xml
# name: mint_system.base.view_partner_tree.show_type
local name="$2"
if [[ "$2" == *.xml ]]; then
local file=$(basename "$2")
name="${file%.xml}"
fi
load-dotenv "$1"
echo "Disable view with name $name"
images/odoo-cli/bin/odoocli --method 'write' --model "ir.ui.view" --domain "[('name', '=', '$name')]" --field "active" --value "False"
}
enable-snippet() {
if [[ -z "$1" ]]; then
echo '$1/env is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/path is empty.'
exit
fi
# Get name from path
local name="$2"
if [[ "$2" == *.xml ]]; then
local file=$(basename "$2")
name="${file%.xml}"
fi
load-dotenv "$1"
echo "Enable view with name $name"
images/odoo-cli/bin/odoocli --method 'write' --model "ir.ui.view" --include-archived --domain "[('name', '=', '$name')]" --field "active" --value "True"
}
remove-snippet() {
if [[ -z "$1" ]]; then
echo '$1/env is empty.'
exit
fi
if [[ -z "$2" ]]; then
echo '$2/path is empty.'
exit
fi
# Get name from path
local name="$2"
if [[ "$2" == *.xml ]]; then
local file=$(basename "$2")
name="${file%.xml}"
fi
load-dotenv "$1"
echo "Remove view with name $name"
images/odoo-cli/bin/odoocli --method "unlink" --model "ir.ui.view" --domain "[('name', '=', '$name')]"
}
lint-snippets() {
echo 'Lint snippets files...'
./bin/lint-snippets
}
# Project commands
load-version() {
if [[ -n "$1" ]]; then
ODOO_VERSION="$1"
fi
echo "Copy .gitmodules 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/version is empty."; exit; fi
ODOO_VERSION="$1"
# Load .gitmodules and .python-version
load-version "$ODOO_VERSION"
# Set .env
load-revision "$ODOO_VERSION"
echo "Run clone-git-folder"
clone-git-folder .
echo "Run checkout-git-folder"
checkout-git-folder "$ODOO_VERSION" .
echo "Run switch-git-folder"
switch-git-folder "$ODOO_VERSION" .
# List git folders
list-git-folder .
}
save-version() {
save-git-folder .
echo "Copy .gitmodules and .python-version to 'versions/$ODOO_VERSION'"
cp ".gitmodules" "versions/$ODOO_VERSION/"
cp ".python-version" "versions/$ODOO_VERSION/"
echo "Remove .gitmoduels.csv."
rm ".gitmodules.csv"
rm "versions/$ODOO_VERSION/.gitmodules.csv"
}
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/revision 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
}
# RUN commands
test-xmlrpc() {
load-dotenv "$1"
if [[ -z "${ODOO_URL}" ]]; then
echo "ODOO_URL is not set."
return 1
fi
if [[ -z "${ODOO_DATABASE}" ]]; then
echo "ODOO_DATABASE is not set."
return 1
fi
if [[ -z "${ODOO_USERNAME}" ]]; then
echo "ODOO_USERNAME is not set."
return 1
fi
if [[ -z "${ODOO_PASSWORD}" ]]; then
echo "ODOO_PASSWORD is not set."
return 1
fi
echo "Connect to database and count res.partner records: $(images/odoo-cli/bin/odoocli --model 'res.partner' --method 'search_count' --domain '[]')"
}
test-json-2() {
load-dotenv "$1"
local url="${ODOO_URL%/}/json/2/res.partner/search_count"
local result
if [[ -z "${ODOO_API_KEY}" ]]; then
echo "ODOO_API_KEY is not set."
echo "Generate an API key in Odoo (User menu > Preferences > Account Security > New API Key)."
return 1
fi
result=$(curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ODOO_API_KEY}" \
-d '{"domain": []}' \
"$url")
echo "Connect to database and count res.partner records: $result"
}
odoobin(){
odoo "$@"
}
odoocli(){
images/odoo-cli/bin/odoocli "$@"
}
manifestoo(){
set-addons-path
./.venv$ODOO_VERSION/bin/manifestoo --addons-path="$ADDONS_PATH" "$@"
}
export-website-data() {
if [[ -n "$1" ]]; then
load-dotenv $1
fi
export ODOO_URL
export DB_NAME
export ODOO_USERNAME
export ODOO_PASSWORD
export ODOO_WEBSITE_ID
echo -e "\nPython Output:\n\n---"
python "bin/export-website-data"
local exit_code=$?
echo -e "---\n"
echo -e "Exit code: $exit_code\n"
}
import-website-data() {
if [[ -n "$1" ]]; then
load-dotenv $1
fi
export ODOO_URL
export DB_NAME
export ODOO_USERNAME
export ODOO_PASSWORD
export ODOO_WEBSITE_ID
echo -e "\nPython Output:\n\n---"
python "bin/import-website-data"
local exit_code=$?
echo -e "---\n"
echo -e "Exit code: $exit_code\n"
}
# Vuepress commands
install-vuepress() {
pnpm install
}
dev-vuepress() {
open-url http://localhost:8080
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
open-url http://localhost:3000
pnpx serve
}
run-vuepress-container() {
container_name="odoo-build"
image_tag="mintsystem/taskfile.build:latest"
docker pull "$image_tag"
docker rm -f "$container_name"
echo "Open http://localhost:8888"
docker run -p 8888:80 -d \
--name "$container_name" \
-e GIT_URL="https://github.com/Mint-System/Odoo-Build" \
-e TASK_COMMANDS="install-vuepress,build-vuepress" \
-e CADDY_ROOT=".vuepress/dist" \
"$image_tag"
}
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 || true
"$1" "${@:2}"
else
case "$1" in
*)
echo "Unknown command: $1"
help
exit 1
;;
esac
fi