Merge branch 'develop'

- support ubuntu 25.10
- fix s390x by removing wkhtmltopdf (it's missing)
- improve selenium_lib.py, support selenium grid project (can be run on
  docker)
- improve code_generator extraction class
- support neutralize database
- fix repository url
- improve postgresql 18 support
This commit is contained in:
Mathieu Benoit 2025-12-20 03:16:03 -05:00
commit 2805f94477
32 changed files with 694 additions and 476 deletions

View file

@ -27,11 +27,13 @@ Recreating the virtual environment, use installation guide from tool `make`.
- Support RobotLibre code generator
- Support ERPLibre DevOps, automation procedure about DevOps
- ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it
- Support Selenium grid from selenium_lib.py
## Changed
- Docker support postgresql 18
- Format script search diff file into each repository
- Support neutralize database from Odoo
## [1.6.0] - 2025-04-25

View file

@ -1,6 +1,11 @@
SHELL := /bin/bash
LOG_FILE := ./.venv.$(cat ".erplibre-version" | xargs)/make_test.log
ODOO_VERSION := $(shell cat .odoo-version | xargs)
ERPLIBRE_VERSION := $(strip \
$(if $(wildcard .erplibre-version),$(shell cat .erplibre-version),) \
)
ODOO_VERSION := $(strip \
$(if $(wildcard .odoo-version),$(shell cat .odoo-version),) \
)
LOG_FILE := ./.venv.$(ERPLIBRE_VERSION)/make_test.log
#############
# General #
#############

View file

@ -111,4 +111,4 @@ docker_clean_all:
.PHONY: docker_compose_clean_all
docker_compose_clean_all:
./script/terminal/validate_to_continue.sh "⚠️ This will REMOVE docker compose images, volumes and network." && docker-compose down --rmi all -v
./script/terminal/validate_to_continue.sh "⚠️ This will REMOVE docker compose images, volumes and network." && docker compose down --rmi all -v

View file

@ -11,6 +11,10 @@ install_dev:
# ./script/install/install_locally_dev.sh
./script/version/update_env_version.py --install_dev
.PHONY: install_erplibre
install_erplibre:
./script/install/install_erplibre.sh
.PHONY: install_odoo_18
install_odoo_18:
./script/version/update_env_version.py --erplibre_version odoo18.0_python3.12.10 --install_dev

View file

@ -1,6 +1,6 @@
services:
ERPLibre:
image: technolibre/erplibre:1.6.0_odoo_18.0_4f4f563
image: technolibre/erplibre:1.6.0_odoo_18.0_2492ba3
ports:
- 8069:8069
- 8071:8071

View file

@ -95,7 +95,7 @@ RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ ${DEBIAN_NAME}-pgdg main"
&& gpgconf --kill all \
&& rm -rf "$GNUPGHOME" \
&& apt-get update \
&& apt-get install --no-install-recommends -y postgresql-client-12 libpq-dev \
&& apt-get install --no-install-recommends -y postgresql-client-18 libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install npm dependencies

View file

@ -26,7 +26,7 @@ services:
volumes:
# See the volume section at the end of the file
- erplibre_data_dir:/home/odoo/.local/share/Odoo
- ./addons/addons:/ERPLibre/odoo12.0/addons/addons
- ./odoo12.0/addons/addons:/ERPLibre/odoo12.0/addons/addons
- ./private:/ERPLibre/private
- erplibre_conf:/etc/odoo
restart: always

View file

@ -26,7 +26,7 @@ services:
volumes:
# See the volume section at the end of the file
- erplibre_data_dir:/home/odoo/.local/share/Odoo
- ./addons/addons:/ERPLibre/odoo14.0/addons/addons
- ./odoo14.0/addons/addons:/ERPLibre/odoo14.0/addons/addons
- ./private:/ERPLibre/private
- erplibre_conf:/etc/odoo
restart: always

View file

@ -26,7 +26,7 @@ services:
volumes:
# See the volume section at the end of the file
- erplibre_data_dir:/home/odoo/.local/share/Odoo
- ./addons/addons:/ERPLibre/odoo16.0/addons/addons
- ./odoo16.0/addons/addons:/ERPLibre/odoo16.0/addons/addons
- ./private:/ERPLibre/private
- erplibre_conf:/etc/odoo
restart: always

View file

@ -1,6 +1,6 @@
services:
ERPLibre:
image: technolibre/erplibre:1.6.0_odoo_18.0_4f4f563
image: technolibre/erplibre:1.6.0_odoo_18.0_2492ba3
ports:
- 8069:8069
- 8071:8071
@ -27,7 +27,7 @@ services:
volumes:
# See the volume section at the end of the file
- erplibre_data_dir:/home/odoo/.local/share/Odoo
- ./addons/addons:/ERPLibre/odoo18.0/addons/addons
- ./odoo18.0/addons/addons:/ERPLibre/odoo18.0/addons/addons
- ./private:/ERPLibre/private
- erplibre_conf:/etc/odoo
restart: always

View file

@ -1,6 +1,6 @@
services:
ERPLibre:
image: technolibre/erplibre:1.6.0_odoo_18.0_4f4f563
image: technolibre/erplibre:1.6.0_odoo_18.0_2492ba3
ports:
- 8069:8069
- 8071:8071
@ -28,7 +28,7 @@ services:
volumes:
# See the volume section at the end of the file
- erplibre_data_dir:/home/odoo/.local/share/Odoo
- ./addons/addons:/ERPLibre/odoo18.0/addons/addons
- ./odoo18.0/addons/addons:/ERPLibre/odoo18.0/addons/addons
- ./private:/ERPLibre/private
- erplibre_conf:/etc/odoo
restart: always

View file

@ -12,7 +12,7 @@
<remote name="TechnoLibre" fetch="https://github.com/TechnoLibre/" />
<remote name="ajepe" fetch="https://github.com/ajepe/" />
<remote name="bemade" fetch="https://git.bemade.org/bemade/" />
<remote name="ERPLibre_origin_bemade" fetch="https://eregion.chezlepro.ca/TechnoLibre/" />
<remote name="ERPLibre_origin_bemade" fetch="https://git.bemade.org/technolibre/" />
<remote name="camptocamp" fetch="https://github.com/camptocamp/" />
<remote name="cetmix" fetch="https://github.com/cetmix/" />
<remote name="dhongu" fetch="https://github.com/dhongu/" />

1
private/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.kdbx

View file

@ -13,4 +13,6 @@ dataclasses
pypiwin32
cadquery
cadquery
pymysql
pymssql

File diff suppressed because it is too large Load diff

View file

@ -21,13 +21,13 @@ authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
python = ">=3.12.10,<3.13"
asn1crypto = "1.5.1"
astor = "^0.8.1"
avalara = "^25.9.0"
avalara = "^25.11.0"
babel = "2.10.3"
bandit = "^1.8.6"
bandit = "^1.9.2"
base64io = "^1.0.3"
beautifulsoup4 = "4.13.5"
bokeh = "3.6.3"
boto3 = "^1.40.74"
boto3 = "^1.42.1"
cachetools = "^6.2.2"
cairosvg = "^2.8.2"
caldav = "1.3.9"
@ -37,7 +37,7 @@ code-writer = "^1.2.0"
colorama = "^0.4.6"
cryptography = "46.0.3"
cssselect = "^1.3.0"
cython = "^3.2.1"
cython = "^3.2.2"
decorator = "5.1.1"
docutils = "0.20.1"
dropbox = "^12.0.2"
@ -45,7 +45,7 @@ email-validator = "^2.3.0"
extract-msg = "^0.55.0"
extruct = "^0.18.0"
ezdxf = "^1.4.3"
factur-x = "^3.13"
factur-x = "^3.14"
filetype = "^1.2.0"
flake8 = "^7.3.0"
formio-data = "^2.1.6"
@ -85,7 +85,7 @@ numpy-financial = "<=1.0.0"
numpy-stl = "^3.2.0"
odoorpc = "^0.10.1"
ofxparse = "0.21"
openai = "^2.8.0"
openai = "^2.8.1"
openpyxl = "3.1.2"
openupgradelib = "^3.12.0"
openwebui-client = ">=0.3.0"
@ -96,11 +96,11 @@ pathspec = "^0.12.1"
pdf2image = "^1.17.0"
pdfminer = "^20191125"
pexpect = "^4.9.0"
phonenumbers = "^9.0.18"
pillow = "10.2.0"
plotly = "^6.4.0"
phonenumbers = "^9.0.19"
pillow = "12.0.0"
plotly = "^6.5.0"
polib = "1.1.1"
pre-commit = "^4.4.0"
pre-commit = "^4.5.0"
premailer = "^3.10.0"
prometheus-client = "^0.23.1"
psutil = "5.9.8"
@ -116,7 +116,6 @@ pyjsparser = "^2.7.1"
pyjwt = "^2.10.1"
pylint = "3.3.9"
pylint-odoo = "^9.3.22"
pymysql = "^1.1.2"
pyncclient = "^0.7"
pyopenssl = "25.3.0"
pypdf2 = "2.12.1"
@ -143,9 +142,9 @@ pyzk = "^0.9"
qifparse = "^0.5"
qrcode = "7.4.2"
radon = "^6.0.1"
redis = "^7.0.1"
redis = "^7.1.0"
reportlab = "4.1.0"
requests = "2.31.0"
requests = ">=2.31.0"
requests-toolbelt = "^1.0.0"
rjsmin = "1.2.0"
roulier = "^1.1.1"
@ -157,7 +156,7 @@ sqlalchemy = "^2.0.44"
statsd = "^4.0.1"
tldextract = "^5.3.0"
tweepy = "^4.16.0"
twilio = "^9.8.6"
twilio = "^9.8.7"
unidecode = "^1.4.0"
urllib3 = "2.0.7"
uvloop = "^0.22.1"
@ -180,9 +179,6 @@ rev = "24.8.0"
extras = [ "s3",]
version = "^2025.10.0"
[tool.poetry.dependencies.pymssql]
git = "https://github.com/pymssql/pymssql.git"
[tool.poetry.dependencies.pysaml2]
git = "https://github.com/prauscher/pysaml2.git"
rev = "replace-pyopenssl"

View file

@ -66,4 +66,6 @@ pyopenssl==25.3.0
git+https://github.com/prauscher/pysaml2.git@replace-pyopenssl
# Fix compilation, until pymssql==2.3.9 is release
git+https://github.com/pymssql/pymssql.git
#git+https://github.com/pymssql/pymssql.git
pillow==12.0.0

View file

@ -319,9 +319,15 @@ def main():
"sequence": sequence,
}
dct_fields[var_name] = d
lst_args = [
a.value for a in node.value.args
]
lst_args = []
for keyword in node.value.args:
value = _fill_search_field(
keyword, var_name
)
# Waste to stock None value
if value is not None:
lst_args.append(value)
if lst_args:
lst_default_arg_position = (
ARGS_TYPE_PARAM.get(
@ -405,6 +411,67 @@ def main():
return 0
def extract_lambda(self, node):
result = ast.unparse(node)
if result[0] == "(" and result[-1] == ")":
result = result[1:-1]
return result
def _fill_search_field(ast_obj, var_name=""):
ast_obj_type = type(ast_obj)
result = None
if ast_obj_type is ast.Lambda:
result = extract_lambda(ast_obj)
elif ast_obj_type is ast.Constant:
result = ast_obj.value
elif ast_obj_type is ast.UnaryOp:
if type(ast_obj.op) is ast.USub:
# value is negative
result = ast_obj.operand.n * -1
else:
_logger.warning(
f"Cannot support keyword of variable {var_name} type"
f" {ast_obj_type} operator {type(ast_obj.op)}."
)
elif ast_obj_type is ast.Name:
result = ast_obj.id
elif ast_obj_type is ast.Attribute:
# Support -> fields.Date.context_today
parent_node = ast_obj
lst_call_lambda = []
if hasattr(parent_node, "id"):
while hasattr(parent_node, "value"):
lst_call_lambda.insert(0, parent_node.attr)
parent_node = parent_node.value
lst_call_lambda.insert(0, parent_node.id)
result = ".".join(lst_call_lambda)
# else:
# # default=uuid.uuid4().hex
# _logger.warning(
# f"Cannot support keyword of variable {var_name} type"
# f" {ast_obj_type} in filename {self.py_filename}, because"
# " parent_node is type ast.Call."
# )
elif ast_obj_type is ast.List:
result = [_fill_search_field(a, var_name) for a in ast_obj.elts]
elif ast_obj_type is ast.Dict:
result = {
_fill_search_field(k, var_name): _fill_search_field(
ast_obj.values[i], var_name
)
for (i, k) in enumerate(ast_obj.keys)
}
elif ast_obj_type is ast.Tuple:
result = tuple([_fill_search_field(a, var_name) for a in ast_obj.elts])
else:
_logger.warning(
f"Cannot support keyword of variable {var_name} type"
f" {ast_obj_type}."
)
return result
if __name__ == "__main__":
status = main()
sys.exit(status)

View file

@ -74,13 +74,15 @@ def main():
no_tab = 0
# Validate file format
out = subprocess.check_output(
out = subprocess.run(
f"python -m tabnanny {config.file}",
stderr=subprocess.STDOUT,
shell=True,
capture_output=True,
text=True,
)
if out:
print(out)
result = (out.stdout or "") + (out.stderr or "")
if result:
print(result)
sys.exit(1)
# with tokenize.open(config.file) as f:

View file

@ -12,8 +12,14 @@ _logger = logging.getLogger(__name__)
def execute_shell(cmd):
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
return out.decode().strip() if out else ""
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
)
output = (result.stdout or "") + (result.stderr or "")
return result.returncode, output.strip()
def get_config():
@ -41,7 +47,7 @@ def get_config():
def main():
config = get_config()
out_db = execute_shell("./odoo_bin.sh db --list")
status, out_db = execute_shell("./odoo_bin.sh db --list")
lst_db = out_db.split("\n")
lst_database_to_delete = []

View file

@ -60,6 +60,11 @@ SUGGESTION
action="store_true",
help="Will only drop database if exist.",
)
parser.add_argument(
"--neutralize",
action="store_true",
help="Will disable all cron.",
)
args = parser.parse_args()
return args
@ -163,6 +168,8 @@ def main():
f"{arg_base} --clone --from_database"
f" {cache_database} --database {config.database}"
)
if config.neutralize:
arg += " --neutralize"
print(arg)
out = check_output(arg.split(" ")).decode()
print(out)

0
script/database/migrate/process_backup_file.py Normal file → Executable file
View file

View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
# -----------------------------
# Parameters
# -----------------------------
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <source_file> [destination_directory]"
exit 1
fi
SRC_FILE="$1"
DEST_DIR="${2:-/home/seluser/Downloads}" # default destination inside container
if [[ ! -f "$SRC_FILE" ]]; then
echo "Error: Source file '$SRC_FILE' does not exist."
exit 1
fi
SRC_BASENAME="$(basename "$SRC_FILE")"
# -----------------------------
# Determine container name
# -----------------------------
CURRENT=$(pwd)
BASENAME=$(basename "${CURRENT}")
# Remove dots and convert to lowercase (same logic as original)
BASENAME="${BASENAME//./}"
BASENAME="${BASENAME,,}"
CANDIDATE1="${BASENAME}-ERPLibre-1"
CANDIDATE2="${BASENAME}_ERPLibre_1"
CONTAINER=""
# Find which container exists
if docker ps --format '{{.Names}}' | grep -qx "${CANDIDATE1}"; then
CONTAINER="${CANDIDATE1}"
elif docker ps --format '{{.Names}}' | grep -qx "${CANDIDATE2}"; then
CONTAINER="${CANDIDATE2}"
else
echo "Error: No matching container found:"
echo " - ${CANDIDATE1}"
echo " - ${CANDIDATE2}"
exit 1
fi
echo "Detected container: ${CONTAINER}"
# -----------------------------
# Copy file into container
# -----------------------------
echo "Ensuring directory exists inside container: ${DEST_DIR}"
docker exec -u root "${CONTAINER}" mkdir -p "${DEST_DIR}"
echo "Copying '${SRC_FILE}' to '${CONTAINER}:${DEST_DIR}/${SRC_BASENAME}'"
docker cp "${SRC_FILE}" "${CONTAINER}:${DEST_DIR}/${SRC_BASENAME}"
echo "File copy complete."

View file

@ -26,7 +26,7 @@ _logger = logging.getLogger(__name__)
PROJECT_NAME = os.path.basename(os.getcwd())
IDEA_PATH = "./.idea"
DEFAULT_ODOO_BIN = "./odoo/odoo-bin" # Will be replaced dynamic
DEFAULT_ODOO_BIN = "./odoo/odoo-bin" # Need this to replace static configuration
IDEA_MISC = os.path.join(IDEA_PATH, "misc.xml")
IDEA_WORKSPACE = os.path.join(IDEA_PATH, "workspace.xml")
VCS_WORKSPACE = os.path.join(IDEA_PATH, "vcs.xml")

View file

@ -62,3 +62,7 @@ install_package cmake
install_package parallel
install_package tk
install_package shfmt
# For erplibre_devops
install_package sshpass

View file

@ -14,7 +14,7 @@ EL_USER=${USER}
UBUNTU_VERSION=$(lsb_release -rs)
DEBIAN_VERSION=$(lsb_release -cs)
OS=$(lsb_release -si)
if [ "25.04" == "${UBUNTU_VERSION}" ] || [ "24.04" == "${UBUNTU_VERSION}" ] || [ "24.10" == "${UBUNTU_VERSION}" ] || [ "23.10" == "${UBUNTU_VERSION}" ] || [ "23.04" == "${UBUNTU_VERSION}" ] || [ "22.10" == "${UBUNTU_VERSION}" ] || [ "22.04" == "${UBUNTU_VERSION}" ]; then
if [ "25.10" == "${UBUNTU_VERSION}" ] || [ "25.04" == "${UBUNTU_VERSION}" ] || [ "24.04" == "${UBUNTU_VERSION}" ] || [ "24.10" == "${UBUNTU_VERSION}" ] || [ "23.10" == "${UBUNTU_VERSION}" ] || [ "23.04" == "${UBUNTU_VERSION}" ] || [ "22.10" == "${UBUNTU_VERSION}" ] || [ "22.04" == "${UBUNTU_VERSION}" ]; then
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
elif [ "20.04" == "${UBUNTU_VERSION}" ]; then
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb
@ -39,7 +39,7 @@ fi
#--------------------------------------------------
if [ "$(uname -m)" = "s390x" ]; then
echo "Arch s390x detected"
sudo apt install wkhtmltopdf rust-all libqpdf-dev libgeos-dev libproj-dev proj-bin proj-data libgeographiclib-dev freetds-dev freetds-bin libkrb5-dev libssl-dev pkg-config build-essential -y
sudo apt install rust-all libqpdf-dev libgeos-dev libproj-dev proj-bin proj-data libgeographiclib-dev freetds-dev freetds-bin libkrb5-dev libssl-dev pkg-config build-essential npm -y
fi
#--------------------------------------------------

View file

@ -3,11 +3,11 @@
if [[ "${OSTYPE}" == "linux-gnu" ]]; then
source /etc/os-release
if [[ "${ID}" == "ubuntu" ]]; then
if [[ "${VERSION_ID}" == "18.04" || "${VERSION_ID}" == "20.04" || "${VERSION_ID}" == "22.04" || "${VERSION_ID}" == "22.10" || "${VERSION_ID}" == "23.04" || "${VERSION_ID}" == "23.10" || "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.04" ]]; then
if [[ "${VERSION_ID}" == "18.04" || "${VERSION_ID}" == "20.04" || "${VERSION_ID}" == "22.04" || "${VERSION_ID}" == "22.10" || "${VERSION_ID}" == "23.04" || "${VERSION_ID}" == "23.10" || "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.04" || "${VERSION_ID}" == "25.10" ]]; then
echo "\n---- linux-gnu installation process started ----"
./script/install/install_debian_dependency.sh
else
echo "Your version is not supported, only support 18.04, 20.04 and 22.04 - 24.04, 25.04 : ${VERSION_ID}"
echo "Your version is not supported, only support 18.04, 20.04 and 22.04 - 24.04, 25.04, 25.10 : ${VERSION_ID}"
fi
elif [[ "${ID}" == "debian" ]]; then
./script/install/install_debian_dependency.sh
@ -15,7 +15,7 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then
./script/install/install_arch_linux.sh
else
./script/install/install_debian_dependency.sh
echo "Your Linux system is not supported, only support Ubuntu 18.04 or Ubuntu 20.04 or Ubuntu 22.04 - Ubuntu 23.10 - Ubuntu 24.04, Ubuntu 25.04 ."
echo "Your Linux system is not supported, only support Ubuntu 18.04 or Ubuntu 20.04 or Ubuntu 22.04 - Ubuntu 23.10 - Ubuntu 24.04, Ubuntu 25.04, Ubuntu 25.10 ."
fi
elif [[ "${OSTYPE}" == "darwin"* ]]; then
echo "\n---- Darwin installation process started ----"

View file

@ -4,25 +4,36 @@
import os
import subprocess
import sys
IGNORE_EXTENSION = []
IGNORE_FILE_NAME = ["Makefile"]
def execute_shell(cmd):
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
return out.decode().strip() if out else ""
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
)
output = (result.stdout or "") + (result.stderr or "")
return result.returncode, output.strip()
def get_modified_files():
lst_cmd_git_status = ["git", "status", "--porcelain"]
lst_cmd_git_status_repo = [
".venv.erplibre/bin/repo",
"forall",
"-p",
"-c",
"git status -s",
]
path_repo_bin = ".venv.erplibre/bin/repo"
if os.path.exists(path_repo_bin):
lst_cmd_git_status_repo = [
path_repo_bin,
"forall",
"-p",
"-c",
"git status -s",
]
else:
lst_cmd_git_status_repo = []
try:
print(" ".join(lst_cmd_git_status))
result = subprocess.run(
@ -35,15 +46,18 @@ def get_modified_files():
lst_lines = [(".", lines_local)]
print(" ".join(lst_cmd_git_status_repo))
if lst_cmd_git_status_repo:
print(" ".join(lst_cmd_git_status_repo))
result = subprocess.run(
lst_cmd_git_status_repo,
capture_output=True,
text=True,
check=True,
)
lines_project = result.stdout.strip().split("\n\n")
result = subprocess.run(
lst_cmd_git_status_repo,
capture_output=True,
text=True,
check=True,
)
lines_project = result.stdout.strip().split("\n\n")
else:
lines_project = []
if os.path.isfile(".odoo-version"):
with open(".odoo-version") as txt:
@ -145,4 +159,7 @@ if __name__ == "__main__":
has_file = True
if has_file:
print(cmd_format)
execute_shell(cmd_format)
status, output = execute_shell(cmd_format)
if status != 0:
print(output)
sys.exit(status)

View file

@ -415,7 +415,7 @@ def combine_requirements(config):
print(f"Internal error, missing result for {lst_requirement}.")
# Support ignored requirements
lst_ignore = get_list_ignored()
lst_ignore, ignore_requirement_file = get_list_ignored()
lst_ignored_key = []
for key in dct_requirements.keys():
for ignored in lst_ignore:
@ -423,6 +423,9 @@ def combine_requirements(config):
lst_ignored_key.append(key)
for key in lst_ignored_key:
del dct_requirements[key]
print(
f"{Fore.YELLOW}WARNING{Style.RESET_ALL} - Ignore '{key}' from '{ignore_requirement_file}'"
)
venv_dir = f".venv.{config.set_version_erplibre}"
with open(f"./{venv_dir}/build_dependency.txt", "w") as f:
@ -521,9 +524,22 @@ def call_poetry_add_build_dependency():
def get_list_ignored():
with open("./requirement/ignore_requirements.txt", "r") as f:
lst_ignore_requirements = [a.strip() for a in f.readlines()]
return lst_ignore_requirements
odoo_erplibre_version = ""
with open("./.erplibre-version", "r", encoding="utf-8") as fichier:
odoo_erplibre_version = fichier.read().strip()
if not odoo_erplibre_version:
_logger.warning(
"Missing ./.erplibre-version file to determine which requirement/ignore_requirement to use."
)
return []
ignore_requirement_file = (
f"./requirement/ignore_requirements.{odoo_erplibre_version}.txt"
)
with open(ignore_requirement_file, "r") as f:
lst_ignore_requirements = [
a.strip() for a in f.readlines() if a.strip()
]
return lst_ignore_requirements, ignore_requirement_file
def main():

View file

@ -4,18 +4,15 @@
import datetime
import getpass
import json
import logging
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
import time
import tkinter as tk
from subprocess import getoutput
from tkinter import filedialog
from pykeepass import PyKeePass
@ -79,7 +76,10 @@ class SeleniumLib(object):
self.config_file = config_file.ConfigFile()
self.kdbx = None
self.video_recorder = None
self.default_download_dir_path = tempfile.mkdtemp()
if config.use_download_path_default:
self.default_download_dir_path = "/home/seluser/Downloads"
else:
self.default_download_dir_path = tempfile.mkdtemp()
if self.config.video_suffix:
self.filename_recording = (
f"video_{self.config.video_suffix}_"
@ -136,9 +136,10 @@ class SeleniumLib(object):
firefox_options.set_preference(
"browser.download.manager.showWhenStarting", False
)
firefox_options.set_preference(
"browser.download.dir", self.default_download_dir_path
)
if self.default_download_dir_path:
firefox_options.set_preference(
"browser.download.dir", self.default_download_dir_path
)
firefox_options.set_preference(
"browser.helperApps.neverAsk.saveToDisk",
"application/octet-stream,application/pdf,application/x-pdf",
@ -248,9 +249,10 @@ class SeleniumLib(object):
firefox_options.set_preference(
"browser.download.manager.showWhenStarting", False
)
firefox_options.set_preference(
"browser.download.dir", self.default_download_dir_path
)
if self.default_download_dir_path:
firefox_options.set_preference(
"browser.download.dir", self.default_download_dir_path
)
firefox_options.set_preference(
"browser.helperApps.neverAsk.saveToDisk",
"application/octet-stream,application/pdf,application/x-pdf",
@ -454,6 +456,7 @@ class SeleniumLib(object):
value: str = None,
timeout=5,
wait_clickable=False,
wait_is_invisible=False,
is_visible=True,
):
only_one = False
@ -476,6 +479,15 @@ class SeleniumLib(object):
)
)
)
elif wait_is_invisible:
ele = wait.until(
EC.invisibility_of_element_located(
(
by,
value,
)
)
)
else:
only_one = True
ele = wait.until(
@ -813,35 +825,59 @@ class SeleniumLib(object):
def scrollto_element(
self,
element,
offset_middle=True,
smooth_scroll=True,
smooth_speed=50,
smooth_fps=1 / 25,
viewport_ele=None,
):
window_y = self.driver.execute_script(
"return window.scrollY || window.pageYOffset;"
)
window_height = self.driver.get_window_size().get("height")
element_height = element.rect["height"]
viewport_height = self.driver.execute_script(
"return window.innerHeight || document.documentElement.clientHeight;"
)
if viewport_ele:
scroll_origin = ScrollOrigin.from_element(viewport_ele, 10, 10)
else:
scroll_origin = ScrollOrigin.from_viewport(10, 10)
delta_y = element.rect["y"]
if offset_middle:
# TODO check size windows and divide by 2
delta_y -= 400
target_distance = (
delta_y - (viewport_height - element_height) / 2 - window_y
)
if window_y < delta_y < window_y + viewport_height - 50:
# No need to scroll
return
print(f"scroll to xpath at y:{delta_y} position")
if smooth_scroll:
actual_pos = 0
while delta_y > actual_pos:
actual_pos += smooth_speed
print(actual_pos)
# Detect go top or bot
if delta_y > window_y:
step = abs(int(smooth_speed)) # to the top
condition = lambda a, d: a < d
go_down = True
else:
step = -abs(int(smooth_speed)) # to the bot
condition = lambda a, d: a > d
go_down = False
while condition(actual_pos, target_distance):
# TODO detect when cannot scroll, stop scrolling
actual_pos += step
if go_down and actual_pos > target_distance:
step = actual_pos - target_distance
elif not go_down and actual_pos < target_distance:
step = actual_pos - target_distance
ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, int(smooth_speed)
scroll_origin, 0, int(step)
).perform()
print(f"Wait {smooth_fps} seconds")
time.sleep(smooth_fps)
print("end")
else:
ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, int(delta_y)
scroll_origin, 0, int(target_distance)
).perform()
# action = ActionChains(self.driver)
@ -1593,6 +1629,15 @@ class SeleniumLib(object):
header.text,
) # on retourne aussi le texte exact pour attendre le changement
def wait_is_invisible(self, by, value):
div_field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.NAME, field_name))
)
wait = WebDriverWait(driver, 10)
wait.until(
EC.invisibility_of_element_located((By.ID, "ui-datepicker-div"))
)
def select_date(self, driver, date_to_select):
# 1) ouvrir/viser le datepicker (si besoin, clique l'input avant)
picker = WebDriverWait(driver, 10).until(
@ -1708,6 +1753,14 @@ def fill_parser(parser):
"--use_network",
help="Specify the adress, example: http://localhost:4444",
)
group_browser.add_argument(
"--use_download_path_default",
action="store_true",
help="Actually, the download path is a temporary directory. "
"This will enable default path to /home/seluser/Downloads, "
"need this with Selenium Grid by network. "
"Will delete all file into /home/seluser/Downloads at startup.",
)
group_browser.add_argument(
"--window_size",
help="Example 1920,1080",

View file

@ -147,9 +147,8 @@ def run(
# Open View
if config.open_dashboard:
selenium_tool.click(
"/html/body/header/nav/div/div[1]/div[2]/div/div/div/ul/li[3]/a",
timeout=15,
selenium_tool.odoo_website_menu_click(
"Tableaux de bord"
)
# Open conversation chat

View file

@ -34,9 +34,6 @@ POETRY_LOCK_FILE = os.path.join("poetry.lock")
POETRY_LOCK_TEMPLATE_FILE = "poetry.%s.lock"
# PIP_REQUIREMENT_FILE = os.path.join("requirements.txt")
PIP_REQUIREMENT_TEMPLATE_FILE = "requirements.%s.txt"
PIP_IGNORE_REQUIREMENT_FILE = os.path.join(
"requirement", "ignore_requirements.txt"
)
PIP_IGNORE_REQUIREMENT_TEMPLATE_FILE = "ignore_requirements.%s.txt"
ADDONS_TEMPLATE_FILE = "odoo%s/addons"
ODOO_TEMPLATE_FILE = "odoo%s"
@ -373,12 +370,6 @@ class Update:
self.expected_poetry_lock_path,
do_delete_source=True,
)
status &= self.update_link_file(
"Pip ignore_requirement.txt",
PIP_IGNORE_REQUIREMENT_FILE,
self.expected_pip_ignore_requirement_path,
do_delete_source=True,
)
if not os.path.isdir(self.expected_addons_name):
status = False
return status