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 RobotLibre code generator
- Support ERPLibre DevOps, automation procedure about DevOps - Support ERPLibre DevOps, automation procedure about DevOps
- ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it - ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it
- Support Selenium grid from selenium_lib.py
## Changed ## Changed
- Docker support postgresql 18 - Docker support postgresql 18
- Format script search diff file into each repository - Format script search diff file into each repository
- Support neutralize database from Odoo
## [1.6.0] - 2025-04-25 ## [1.6.0] - 2025-04-25

View file

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

View file

@ -111,4 +111,4 @@ docker_clean_all:
.PHONY: docker_compose_clean_all .PHONY: docker_compose_clean_all
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/install/install_locally_dev.sh
./script/version/update_env_version.py --install_dev ./script/version/update_env_version.py --install_dev
.PHONY: install_erplibre
install_erplibre:
./script/install/install_erplibre.sh
.PHONY: install_odoo_18 .PHONY: install_odoo_18
install_odoo_18: install_odoo_18:
./script/version/update_env_version.py --erplibre_version odoo18.0_python3.12.10 --install_dev ./script/version/update_env_version.py --erplibre_version odoo18.0_python3.12.10 --install_dev

View file

@ -1,6 +1,6 @@
services: services:
ERPLibre: ERPLibre:
image: technolibre/erplibre:1.6.0_odoo_18.0_4f4f563 image: technolibre/erplibre:1.6.0_odoo_18.0_2492ba3
ports: ports:
- 8069:8069 - 8069:8069
- 8071:8071 - 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 \ && gpgconf --kill all \
&& rm -rf "$GNUPGHOME" \ && rm -rf "$GNUPGHOME" \
&& apt-get update \ && 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/* && rm -rf /var/lib/apt/lists/*
# Install npm dependencies # Install npm dependencies

View file

@ -26,7 +26,7 @@ services:
volumes: volumes:
# See the volume section at the end of the file # See the volume section at the end of the file
- erplibre_data_dir:/home/odoo/.local/share/Odoo - 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 - ./private:/ERPLibre/private
- erplibre_conf:/etc/odoo - erplibre_conf:/etc/odoo
restart: always restart: always

View file

@ -26,7 +26,7 @@ services:
volumes: volumes:
# See the volume section at the end of the file # See the volume section at the end of the file
- erplibre_data_dir:/home/odoo/.local/share/Odoo - 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 - ./private:/ERPLibre/private
- erplibre_conf:/etc/odoo - erplibre_conf:/etc/odoo
restart: always restart: always

View file

@ -26,7 +26,7 @@ services:
volumes: volumes:
# See the volume section at the end of the file # See the volume section at the end of the file
- erplibre_data_dir:/home/odoo/.local/share/Odoo - 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 - ./private:/ERPLibre/private
- erplibre_conf:/etc/odoo - erplibre_conf:/etc/odoo
restart: always restart: always

View file

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

View file

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

View file

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

View file

@ -66,4 +66,6 @@ pyopenssl==25.3.0
git+https://github.com/prauscher/pysaml2.git@replace-pyopenssl git+https://github.com/prauscher/pysaml2.git@replace-pyopenssl
# Fix compilation, until pymssql==2.3.9 is release # 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, "sequence": sequence,
} }
dct_fields[var_name] = d dct_fields[var_name] = d
lst_args = [ lst_args = []
a.value for a in node.value.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: if lst_args:
lst_default_arg_position = ( lst_default_arg_position = (
ARGS_TYPE_PARAM.get( ARGS_TYPE_PARAM.get(
@ -405,6 +411,67 @@ def main():
return 0 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__": if __name__ == "__main__":
status = main() status = main()
sys.exit(status) sys.exit(status)

View file

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

View file

@ -12,8 +12,14 @@ _logger = logging.getLogger(__name__)
def execute_shell(cmd): def execute_shell(cmd):
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) result = subprocess.run(
return out.decode().strip() if out else "" cmd,
shell=True,
capture_output=True,
text=True,
)
output = (result.stdout or "") + (result.stderr or "")
return result.returncode, output.strip()
def get_config(): def get_config():
@ -41,7 +47,7 @@ def get_config():
def main(): def main():
config = get_config() 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_db = out_db.split("\n")
lst_database_to_delete = [] lst_database_to_delete = []

View file

@ -60,6 +60,11 @@ SUGGESTION
action="store_true", action="store_true",
help="Will only drop database if exist.", help="Will only drop database if exist.",
) )
parser.add_argument(
"--neutralize",
action="store_true",
help="Will disable all cron.",
)
args = parser.parse_args() args = parser.parse_args()
return args return args
@ -163,6 +168,8 @@ def main():
f"{arg_base} --clone --from_database" f"{arg_base} --clone --from_database"
f" {cache_database} --database {config.database}" f" {cache_database} --database {config.database}"
) )
if config.neutralize:
arg += " --neutralize"
print(arg) print(arg)
out = check_output(arg.split(" ")).decode() out = check_output(arg.split(" ")).decode()
print(out) 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()) PROJECT_NAME = os.path.basename(os.getcwd())
IDEA_PATH = "./.idea" 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_MISC = os.path.join(IDEA_PATH, "misc.xml")
IDEA_WORKSPACE = os.path.join(IDEA_PATH, "workspace.xml") IDEA_WORKSPACE = os.path.join(IDEA_PATH, "workspace.xml")
VCS_WORKSPACE = os.path.join(IDEA_PATH, "vcs.xml") VCS_WORKSPACE = os.path.join(IDEA_PATH, "vcs.xml")

View file

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

View file

@ -14,7 +14,7 @@ EL_USER=${USER}
UBUNTU_VERSION=$(lsb_release -rs) UBUNTU_VERSION=$(lsb_release -rs)
DEBIAN_VERSION=$(lsb_release -cs) DEBIAN_VERSION=$(lsb_release -cs)
OS=$(lsb_release -si) 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 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 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 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 if [ "$(uname -m)" = "s390x" ]; then
echo "Arch s390x detected" 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 fi
#-------------------------------------------------- #--------------------------------------------------

View file

@ -3,11 +3,11 @@
if [[ "${OSTYPE}" == "linux-gnu" ]]; then if [[ "${OSTYPE}" == "linux-gnu" ]]; then
source /etc/os-release source /etc/os-release
if [[ "${ID}" == "ubuntu" ]]; then 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 ----" echo "\n---- linux-gnu installation process started ----"
./script/install/install_debian_dependency.sh ./script/install/install_debian_dependency.sh
else 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 fi
elif [[ "${ID}" == "debian" ]]; then elif [[ "${ID}" == "debian" ]]; then
./script/install/install_debian_dependency.sh ./script/install/install_debian_dependency.sh
@ -15,7 +15,7 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then
./script/install/install_arch_linux.sh ./script/install/install_arch_linux.sh
else else
./script/install/install_debian_dependency.sh ./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 fi
elif [[ "${OSTYPE}" == "darwin"* ]]; then elif [[ "${OSTYPE}" == "darwin"* ]]; then
echo "\n---- Darwin installation process started ----" echo "\n---- Darwin installation process started ----"

View file

@ -4,25 +4,36 @@
import os import os
import subprocess import subprocess
import sys
IGNORE_EXTENSION = [] IGNORE_EXTENSION = []
IGNORE_FILE_NAME = ["Makefile"] IGNORE_FILE_NAME = ["Makefile"]
def execute_shell(cmd): def execute_shell(cmd):
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) result = subprocess.run(
return out.decode().strip() if out else "" cmd,
shell=True,
capture_output=True,
text=True,
)
output = (result.stdout or "") + (result.stderr or "")
return result.returncode, output.strip()
def get_modified_files(): def get_modified_files():
lst_cmd_git_status = ["git", "status", "--porcelain"] lst_cmd_git_status = ["git", "status", "--porcelain"]
lst_cmd_git_status_repo = [ path_repo_bin = ".venv.erplibre/bin/repo"
".venv.erplibre/bin/repo", if os.path.exists(path_repo_bin):
"forall", lst_cmd_git_status_repo = [
"-p", path_repo_bin,
"-c", "forall",
"git status -s", "-p",
] "-c",
"git status -s",
]
else:
lst_cmd_git_status_repo = []
try: try:
print(" ".join(lst_cmd_git_status)) print(" ".join(lst_cmd_git_status))
result = subprocess.run( result = subprocess.run(
@ -35,15 +46,18 @@ def get_modified_files():
lst_lines = [(".", lines_local)] 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( result = subprocess.run(
lst_cmd_git_status_repo, lst_cmd_git_status_repo,
capture_output=True, capture_output=True,
text=True, text=True,
check=True, check=True,
) )
lines_project = result.stdout.strip().split("\n\n") lines_project = result.stdout.strip().split("\n\n")
else:
lines_project = []
if os.path.isfile(".odoo-version"): if os.path.isfile(".odoo-version"):
with open(".odoo-version") as txt: with open(".odoo-version") as txt:
@ -145,4 +159,7 @@ if __name__ == "__main__":
has_file = True has_file = True
if has_file: if has_file:
print(cmd_format) 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}.") print(f"Internal error, missing result for {lst_requirement}.")
# Support ignored requirements # Support ignored requirements
lst_ignore = get_list_ignored() lst_ignore, ignore_requirement_file = get_list_ignored()
lst_ignored_key = [] lst_ignored_key = []
for key in dct_requirements.keys(): for key in dct_requirements.keys():
for ignored in lst_ignore: for ignored in lst_ignore:
@ -423,6 +423,9 @@ def combine_requirements(config):
lst_ignored_key.append(key) lst_ignored_key.append(key)
for key in lst_ignored_key: for key in lst_ignored_key:
del dct_requirements[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}" venv_dir = f".venv.{config.set_version_erplibre}"
with open(f"./{venv_dir}/build_dependency.txt", "w") as f: 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(): def get_list_ignored():
with open("./requirement/ignore_requirements.txt", "r") as f: odoo_erplibre_version = ""
lst_ignore_requirements = [a.strip() for a in f.readlines()] with open("./.erplibre-version", "r", encoding="utf-8") as fichier:
return lst_ignore_requirements 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(): def main():

View file

@ -4,18 +4,15 @@
import datetime import datetime
import getpass import getpass
import json
import logging import logging
import os import os
import random import random
import re import re
import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time import time
import tkinter as tk import tkinter as tk
from subprocess import getoutput
from tkinter import filedialog from tkinter import filedialog
from pykeepass import PyKeePass from pykeepass import PyKeePass
@ -79,7 +76,10 @@ class SeleniumLib(object):
self.config_file = config_file.ConfigFile() self.config_file = config_file.ConfigFile()
self.kdbx = None self.kdbx = None
self.video_recorder = 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: if self.config.video_suffix:
self.filename_recording = ( self.filename_recording = (
f"video_{self.config.video_suffix}_" f"video_{self.config.video_suffix}_"
@ -136,9 +136,10 @@ class SeleniumLib(object):
firefox_options.set_preference( firefox_options.set_preference(
"browser.download.manager.showWhenStarting", False "browser.download.manager.showWhenStarting", False
) )
firefox_options.set_preference( if self.default_download_dir_path:
"browser.download.dir", self.default_download_dir_path firefox_options.set_preference(
) "browser.download.dir", self.default_download_dir_path
)
firefox_options.set_preference( firefox_options.set_preference(
"browser.helperApps.neverAsk.saveToDisk", "browser.helperApps.neverAsk.saveToDisk",
"application/octet-stream,application/pdf,application/x-pdf", "application/octet-stream,application/pdf,application/x-pdf",
@ -248,9 +249,10 @@ class SeleniumLib(object):
firefox_options.set_preference( firefox_options.set_preference(
"browser.download.manager.showWhenStarting", False "browser.download.manager.showWhenStarting", False
) )
firefox_options.set_preference( if self.default_download_dir_path:
"browser.download.dir", self.default_download_dir_path firefox_options.set_preference(
) "browser.download.dir", self.default_download_dir_path
)
firefox_options.set_preference( firefox_options.set_preference(
"browser.helperApps.neverAsk.saveToDisk", "browser.helperApps.neverAsk.saveToDisk",
"application/octet-stream,application/pdf,application/x-pdf", "application/octet-stream,application/pdf,application/x-pdf",
@ -454,6 +456,7 @@ class SeleniumLib(object):
value: str = None, value: str = None,
timeout=5, timeout=5,
wait_clickable=False, wait_clickable=False,
wait_is_invisible=False,
is_visible=True, is_visible=True,
): ):
only_one = False 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: else:
only_one = True only_one = True
ele = wait.until( ele = wait.until(
@ -813,35 +825,59 @@ class SeleniumLib(object):
def scrollto_element( def scrollto_element(
self, self,
element, element,
offset_middle=True,
smooth_scroll=True, smooth_scroll=True,
smooth_speed=50, smooth_speed=50,
smooth_fps=1 / 25, smooth_fps=1 / 25,
viewport_ele=None, 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: if viewport_ele:
scroll_origin = ScrollOrigin.from_element(viewport_ele, 10, 10) scroll_origin = ScrollOrigin.from_element(viewport_ele, 10, 10)
else: else:
scroll_origin = ScrollOrigin.from_viewport(10, 10) scroll_origin = ScrollOrigin.from_viewport(10, 10)
delta_y = element.rect["y"] delta_y = element.rect["y"]
if offset_middle: target_distance = (
# TODO check size windows and divide by 2 delta_y - (viewport_height - element_height) / 2 - window_y
delta_y -= 400 )
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") print(f"scroll to xpath at y:{delta_y} position")
if smooth_scroll: if smooth_scroll:
actual_pos = 0 actual_pos = 0
while delta_y > actual_pos: # Detect go top or bot
actual_pos += smooth_speed if delta_y > window_y:
print(actual_pos) 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( ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, int(smooth_speed) scroll_origin, 0, int(step)
).perform() ).perform()
print(f"Wait {smooth_fps} seconds") print(f"Wait {smooth_fps} seconds")
time.sleep(smooth_fps) time.sleep(smooth_fps)
print("end") print("end")
else: else:
ActionChains(self.driver).scroll_from_origin( ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, int(delta_y) scroll_origin, 0, int(target_distance)
).perform() ).perform()
# action = ActionChains(self.driver) # action = ActionChains(self.driver)
@ -1593,6 +1629,15 @@ class SeleniumLib(object):
header.text, header.text,
) # on retourne aussi le texte exact pour attendre le changement ) # 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): def select_date(self, driver, date_to_select):
# 1) ouvrir/viser le datepicker (si besoin, clique l'input avant) # 1) ouvrir/viser le datepicker (si besoin, clique l'input avant)
picker = WebDriverWait(driver, 10).until( picker = WebDriverWait(driver, 10).until(
@ -1708,6 +1753,14 @@ def fill_parser(parser):
"--use_network", "--use_network",
help="Specify the adress, example: http://localhost:4444", 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( group_browser.add_argument(
"--window_size", "--window_size",
help="Example 1920,1080", help="Example 1920,1080",

View file

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

View file

@ -34,9 +34,6 @@ POETRY_LOCK_FILE = os.path.join("poetry.lock")
POETRY_LOCK_TEMPLATE_FILE = "poetry.%s.lock" POETRY_LOCK_TEMPLATE_FILE = "poetry.%s.lock"
# PIP_REQUIREMENT_FILE = os.path.join("requirements.txt") # PIP_REQUIREMENT_FILE = os.path.join("requirements.txt")
PIP_REQUIREMENT_TEMPLATE_FILE = "requirements.%s.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" PIP_IGNORE_REQUIREMENT_TEMPLATE_FILE = "ignore_requirements.%s.txt"
ADDONS_TEMPLATE_FILE = "odoo%s/addons" ADDONS_TEMPLATE_FILE = "odoo%s/addons"
ODOO_TEMPLATE_FILE = "odoo%s" ODOO_TEMPLATE_FILE = "odoo%s"
@ -373,12 +370,6 @@ class Update:
self.expected_poetry_lock_path, self.expected_poetry_lock_path,
do_delete_source=True, 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): if not os.path.isdir(self.expected_addons_name):
status = False status = False
return status return status