Merge branch 'develop'

- add repository project odoo 18
- improve selenium
- script install with debug
- support queue_job when running robotlibre
- mobile support context name and rename repo
- script to kill odoo process from TODO
This commit is contained in:
Mathieu Benoit 2026-02-04 09:21:10 -05:00
commit da227b8d28
23 changed files with 2409 additions and 947 deletions

2
.gitignore vendored
View file

@ -49,4 +49,4 @@ screenshots
screencasts
private/todo/todo_override_private.json
.erplibre.error.txt
/mobile/technolibre_home_mobile/
/mobile/erplibre_home_mobile/

View file

@ -23,6 +23,7 @@ robot_libre_init:
.PHONY: robot_libre_pre
robot_libre_pre:
echo "Create database robotlibre"
./script/config/setup_odoo_config_conf_devops.py
./script/database/db_restore.py --database robotlibre
.PHONY: robot_libre

View file

@ -30,7 +30,7 @@ sudo snap install core
sudo snap refresh core
# https://certbot.eff.org/lets-encrypt/debianbuster-nginx
# Cerbot
# Certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
```

View file

@ -69,6 +69,8 @@ services:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
# - SE_NODE_OVERRIDE_MAX_SESSIONS=true
# - SE_NODE_MAX_SESSIONS=2
# put into config.conf selenium_network_url=http://selenium-hub:4444
# in python

View file

@ -4,7 +4,7 @@
<project
name="technolibre_home_mobile.git"
path="mobile/technolibre_home_mobile"
path="mobile/erplibre_home_mobile"
remote="TechnoLibre"
revision="main"
groups="mobile"

View file

@ -284,6 +284,13 @@
remote="ERPLibre_origin_MathBenTech"
groups="addons,base,odoo16.0"
/>
<project
name="design-themes.git"
path="odoo16.0/addons/odoo_design-themes"
remote="ERPLibre_origin_odoo"
revision="16.0"
groups="addons,odoo16.0"
/>
<project
name="donation.git"
revision="16.0"

View file

@ -262,6 +262,13 @@
remote="ERPLibre_origin_MathBenTech"
groups="addons,base,odoo17.0"
/>
<project
name="design-themes.git"
path="odoo17.0/addons/odoo_design-themes"
remote="ERPLibre_origin_odoo"
revision="17.0"
groups="addons,odoo17.0"
/>
<project
name="donation.git"
path="odoo17.0/addons/OCA_donation"

View file

@ -6,8 +6,10 @@
<remote name="ERPLibre_origin_OCA" fetch="https://github.com/ERPLibre/" />
<remote name="ERPLibre_origin_CybroOdoo" fetch="https://github.com/ERPLibre/" />
<remote name="ERPLibre_origin_TechnoLibre" fetch="https://github.com/ERPLibre/" />
<remote name="ERPLibre_origin_ONLYOFFICE" fetch="https://github.com/ERPLibre/" />
<remote name="Numigi" fetch="https://github.com/Numigi/" />
<remote name="OCA" fetch="https://github.com/OCA/" />
<remote name="ONLYOFFICE" fetch="https://github.com/ONLYOFFICE" />
<remote name="OmniaGit" fetch="https://github.com/OmniaGit/" />
<remote name="TechnoLibre" fetch="https://github.com/TechnoLibre/" />
<remote name="ajepe" fetch="https://github.com/ajepe/" />
@ -55,9 +57,9 @@
/>
<project
name="account-budgeting.git"
revision="18.0-mig-account_budget_oca"
revision="18.0"
path="odoo18.0/addons/OCA_account-budgeting"
remote="ERPLibre_origin_OCA"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
@ -123,6 +125,20 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="agreement.git"
revision="18.0"
path="odoo18.0/addons/OCA_agreement"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="ai.git"
revision="18.0"
path="odoo18.0/addons/OCA_ai"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="apps-store.git"
revision="18.0"
@ -130,6 +146,13 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="automation.git"
revision="18.0"
path="odoo18.0/addons/OCA_automation"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="bank-payment.git"
revision="18.0"
@ -180,7 +203,13 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<!-- <project name="cetmix-tower.git" revision="18.0" path="odoo18.0/addons/cetmix_cetmix-tower" remote="cetmix" groups="addons,odoo18.0" />-->
<project
name="cetmix-tower.git"
revision="18.0"
path="odoo18.0/addons/cetmix_cetmix-tower"
remote="cetmix"
groups="addons,odoo18.0"
/>
<project
name="community-data-files.git"
revision="18.0"
@ -282,6 +311,13 @@
remote="ERPLibre_origin_MathBenTech"
groups="addons,base,odoo18.0"
/>
<project
name="design-themes.git"
path="odoo18.0/addons/odoo_design-themes"
remote="ERPLibre_origin_odoo"
revision="18.0"
groups="addons,odoo18.0"
/>
<project
name="donation.git"
revision="18.0"
@ -328,6 +364,8 @@
groups="addons,odoo18.0"
/>
<project name="hr.git" revision="18.0" path="odoo18.0/addons/OCA_hr" remote="OCA" groups="addons,odoo18.0" />
<project name="hr-attendance.git" revision="18.0" path="odoo18.0/addons/OCA_hr-attendance" remote="OCA" groups="addons,odoo18.0" />
<project name="hr-expense.git" revision="18.0" path="odoo18.0/addons/OCA_hr-expense" remote="OCA" groups="addons,odoo18.0" />
<project
name="hr-holidays.git"
revision="18.0"
@ -335,6 +373,20 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="infrastructure.git"
revision="18.0"
path="odoo18.0/addons/OCA_infrastructure"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="intrastat-extrastat.git"
revision="18.0"
path="odoo18.0/addons/OCA_intrastat-extrastat"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="interface-github.git"
revision="18.0"
@ -342,6 +394,13 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="iot.git"
revision="18.0"
path="odoo18.0/addons/OCA_iot"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="knowledge.git"
revision="18.0"
@ -364,6 +423,7 @@
groups="addons,odoo18.0"
/>
<project name="mail.git" revision="18.0" path="odoo18.0/addons/OCA_mail" remote="OCA" groups="addons,odoo18.0" />
<project name="mass-mailing.git" revision="18.0" path="odoo18.0/addons/OCA_mass-mailing" remote="OCA" groups="addons,odoo18.0" />
<project
name="management-system.git"
revision="18.0"
@ -378,6 +438,13 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="manufacture-reporting.git"
revision="18.0"
path="odoo18.0/addons/OCA_manufacture-reporting"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="margin-analysis.git"
revision="18.0"
@ -400,6 +467,13 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="muk-it_odoo-modules.git"
path="odoo18.0/addons/muk-it_odoo-modules"
remote="TechnoLibre_origin_muk-it"
revision="18.0"
groups="addons,odoo18.0"
/>
<!-- <project name="muk_web.git" revision="18.0" path="odoo18.0/addons/muk-it_muk_web" remote="muk-it" groups="addons,odoo18.0" />-->
<!-- <project-->
<!-- name="odoo-business-spending-management-quebec-canada.git"-->
@ -436,10 +510,10 @@
groups="addons,odoo18.0"
/>
<project
name="muk-it_odoo-modules.git"
path="odoo18.0/addons/muk-it_odoo-modules"
remote="TechnoLibre_origin_muk-it"
name="odoo-pim.git"
revision="18.0"
path="odoo18.0/addons/OCA_odoo-pim"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
@ -464,6 +538,13 @@
remote="OmniaGit"
groups="addons,odoo18.0"
/>
<project
name="onlyoffice_odoo.git"
revision="18.0"
path="odoo18.0/addons/ONLYOFFICE_onlyoffice_odoo"
remote="ERPLibre_origin_ONLYOFFICE"
groups="addons,odoo18.0"
/>
<project
name="openeducat_erp.git"
revision="18.0"
@ -485,6 +566,13 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="payroll.git"
revision="18.0"
path="odoo18.0/addons/OCA_payroll"
remote="OCA"
groups="addons,odoo18.0"
/>
<project name="pos.git" revision="18.0" path="odoo18.0/addons/OCA_pos" remote="OCA" groups="addons,odoo18.0" />
<project
name="product-attribute.git"
@ -535,6 +623,13 @@
remote="ERPLibre_origin_OCA"
groups="addons,odoo18.0"
/>
<project
name="purchase-reporting.git"
revision="18.0"
path="odoo18.0/addons/OCA_purchase-reporting"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="purchase-workflow.git"
revision="18.0"
@ -543,6 +638,20 @@
groups="addons,odoo18.0"
/>
<project name="queue.git" revision="18.0" path="odoo18.0/addons/OCA_queue" remote="OCA" groups="addons,odoo18.0" />
<project
name="repair.git"
revision="18.0"
path="odoo18.0/addons/OCA_repair"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="report-print-send.git"
revision="18.0"
path="odoo18.0/addons/OCA_report-print-send"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="reporting-engine.git"
revision="18.0"
@ -550,6 +659,13 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="rest-framework.git"
revision="18.0"
path="odoo18.0/addons/OCA_rest-framework"
remote="OCA"
groups="addons,odoo18.0"
/>
<project name="rma.git" revision="18.0" path="odoo18.0/addons/OCA_rma" remote="OCA" groups="addons,odoo18.0" />
<!-- <project-->
<!-- name="saas-addons.git"-->
@ -557,6 +673,27 @@
<!-- remote="it-projects-llc"-->
<!-- groups="addons,odoo18.0"-->
<!-- />-->
<project
name="sale-blanket.git"
revision="18.0"
path="odoo18.0/addons/OCA_sale-blanket"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="sale-channel.git"
revision="18.0"
path="odoo18.0/addons/OCA_sale-channel"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="sale-promotion.git"
revision="18.0"
path="odoo18.0/addons/OCA_sale-promotion"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="sale-reporting.git"
revision="18.0"
@ -613,13 +750,13 @@
remote="ERPLibre_origin_OCA"
groups="addons,odoo18.0"
/>
<!-- <project-->
<!-- name="shift-planning.git"-->
<!-- revision="18.0" path="odoo18.0/addons/OCA_shift-planning"-->
<!-- remote="ERPLibre"-->
<!-- revision="18.0_dev"-->
<!-- groups="addons,odoo18.0"-->
<!-- />-->
<project
name="shift-planning.git"
revision="18.0"
path="odoo18.0/addons/OCA_shift-planning"
remote="OCA"
groups="addons,odoo18.0"
/>
<project name="sign.git" revision="18.0" path="odoo18.0/addons/OCA_sign" remote="OCA" groups="addons,odoo18.0" />
<project
name="spreadsheet.git"
@ -628,6 +765,13 @@
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="shopfloor-app.git"
revision="18.0"
path="odoo18.0/addons/OCA_shopfloor-app"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="social.git"
revision="18.0"
@ -705,6 +849,13 @@
remote="ERPLibre_origin_OCA"
groups="addons,odoo18.0"
/>
<project
name="web-api.git"
revision="18.0"
path="odoo18.0/addons/OCA_web-api"
remote="OCA"
groups="addons,odoo18.0"
/>
<project
name="website-cms.git"
revision="18.0"

View file

@ -1,16 +1,11 @@
#!/usr/bin/env bash
if [[ ! -d "./mobile/technolibre_home_mobile" ]]; then
if [[ ! -d "./mobile/erplibre_home_mobile" ]]; then
echo "Please, run installation ./mobile/install_mobile_dev.sh before run this script ./mobile/compile_and_run.sh"
exit 1
fi
if [[ ! -d "./mobile/technolibre_home_mobile/technolibre_home" ]]; then
echo "Please, check repo is working."
exit 1
fi
cd mobile/technolibre_home_mobile/technolibre_home
cd mobile/erplibre_home_mobile
npm install
npm run build && npx cap sync

View file

@ -29,6 +29,10 @@ python-dateutil
unidecode
sshconf
cloudflare
psutil
# For mobile
pillow
# Force upgrade package to fix Ubuntu 25.04
virtualenv==20.30.0

File diff suppressed because it is too large Load diff

View file

@ -19,33 +19,44 @@ authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
[tool.poetry.dependencies]
python = ">=3.12.10,<3.13"
a2wsgi = ">=1.10.6"
ansi2html = "^1.9.2"
apispec = "^6.9.0"
asn1crypto = "1.5.1"
astor = "^0.8.1"
avalara = "^25.11.0"
avalara = "^25.12.0"
babel = "2.10.3"
bandit = "^1.9.2"
bandit = "^1.9.3"
base64io = "^1.0.3"
beautifulsoup4 = "4.13.5"
bokeh = "3.6.3"
boto3 = "^1.42.1"
cachetools = "^6.2.2"
boto3 = "^1.42.34"
cachetools = "^6.2.4"
cairosvg = "^2.8.2"
caldav = "1.3.9"
cbor2 = "5.6.2"
cerberus = "^1.3.8"
chardet = "5.2.0"
code-writer = "^1.2.0"
code-writer = "^1.3.0"
colorama = "^0.4.6"
contextvars = "^2.4"
cryptography = "46.0.3"
cssselect = "^1.3.0"
cython = "^3.2.2"
cython = "^3.2.4"
dateparser = "^1.2.2"
decorator = "5.1.1"
dnspython = "^2.8.0"
docutils = "0.20.1"
dropbox = "^12.0.2"
easypost = "7.15.0"
email-validator = "^2.3.0"
extendable = ">=0.0.4"
extendable-pydantic = ">=1.2.0"
extract-msg = "^0.55.0"
extruct = "^0.18.0"
ezdxf = "^1.4.3"
factur-x = "^3.14"
factur-x = "^3.15"
fastapi = ">=0.110.0"
filetype = "^1.2.0"
flake8 = "^7.3.0"
formio-data = "^2.1.6"
@ -54,7 +65,8 @@ geocoder = "^1.38.1"
geoip2 = "2.9.0"
geojson = "^3.2.0"
gevent = "24.2.1"
gitpython = "^3.1.45"
gitpython = "^3.1.46"
giturlparse = "0.12.0"
googletrans-py = "4.0.0"
greenlet = "3.0.3"
httpagentparser = "^1.9.5"
@ -72,7 +84,7 @@ mako = "^1.3.10"
markdown2 = "2.5.1"
markdownify = "0.13.1"
markupsafe = "2.1.5"
matplotlib = "^3.10.7"
matplotlib = "^3.10.8"
meteostat = "1.6.8"
mock = "^5.2.0"
mpld3 = "0.5.10"
@ -83,43 +95,53 @@ num2words = "0.5.13"
numpy = ">=1.15"
numpy-financial = "<=1.0.0"
numpy-stl = "^3.2.0"
oauthlib = "^3.3.1"
odoorpc = "^0.10.1"
ofxparse = "0.21"
openai = "^2.8.1"
ollama = "^0.6.1"
openai = "^2.15.0"
openpyxl = "3.1.2"
openupgradelib = "^3.12.0"
openwebui-client = ">=0.3.0"
ovh = "^1.2.0"
pandas = ">=0.25.3"
paramiko = "<4.0.0"
parse-accept-language = "^0.1.2"
passlib = "1.7.4"
pathspec = "^0.12.1"
pathspec = "^1.0.3"
pdf2image = "^1.17.0"
pdfminer = "^20191125"
pexpect = "^4.9.0"
phonenumbers = "^9.0.19"
phonenumbers = "^9.0.22"
pillow = "12.0.0"
plotly = "^6.5.0"
plaid-python = "^38.0.0"
plotly = "^6.5.2"
polib = "1.1.1"
pre-commit = "^4.5.0"
pre-commit = "^4.5.1"
premailer = "^3.10.0"
prometheus-client = "^0.23.1"
prometheus-client = "^0.24.1"
psutil = "5.9.8"
psycopg2 = "2.9.9"
py3dns = "^4.0.2"
py3o-formats = "^0.3"
py3o-template = "^0.10.0"
pycountry = "^24.6.1"
pycups = "^2.0.4"
pydantic = ">=2.0.0"
pydevd-odoo = "^1.2.2"
pyftpdlib = "^2.1.0"
pygithub = "^2.8.1"
pyjsparser = "^2.7.1"
pyjwt = "^2.10.1"
pylint = "3.3.9"
pylint-odoo = "^9.3.22"
pylint = "^4.0.4"
pylint-odoo = "^10.0.0"
pymupdf = "^1.26.7"
pyncclient = "^0.7"
pyopenssl = "25.3.0"
pypdf = ">=3.1.0"
pypdf2 = "2.12.1"
pyproj = "^3.7.2"
pyquerystring = "^1.1"
pyserial = "3.5"
pysftp = "^0.2.9"
pytesseract = "^0.3.13"
@ -129,11 +151,12 @@ python-dotenv = ">=1.0.0"
python-jose = "^3.5.0"
python-json-logger = "^4.0.0"
python-ldap = "3.4.4"
python-multipart = "^0.0.21"
python-pptx = "^1.0.2"
python-resize-image = "^1.1.20"
python-slugify = "^8.0.4"
python-stdnum = "1.19"
python-telegram-bot = "^22.5"
python-telegram-bot = "^22.6"
pytz = "^2025.2"
pyusb = "1.2.1"
pyyaml = "^6.0.3"
@ -143,24 +166,30 @@ qifparse = "^0.5"
qrcode = "7.4.2"
radon = "^6.0.1"
redis = "^7.1.0"
regex = "^2026.1.15"
reportlab = "4.1.0"
requests = ">=2.31.0"
requests-oauthlib = "^2.0.0"
requests-toolbelt = "^1.0.0"
responses = "^0.25.8"
rjsmin = "1.2.0"
roulier = "^1.1.1"
schwifty = "2024.4.0"
sentry-sdk = ">=2.0.0,<=2.22.0"
setuptools = "80.9.0"
shapely = "^2.1.2"
sqlalchemy = "^2.0.44"
sqlalchemy = "^2.0.46"
statsd = "^4.0.1"
tldextract = "^5.3.0"
tldextract = "^5.3.1"
tweepy = "^4.16.0"
twilio = "^9.8.7"
twilio = "^9.10.0"
typing-extensions = "^4.15.0"
ujson = "^5.11.0"
unidecode = "^1.4.0"
urllib3 = "2.0.7"
uvloop = "^0.22.1"
validate-email = "^1.3"
validator-collection = "^1.5.0"
vobject = "0.9.6.1"
w3lib = "^2.3.1"
werkzeug = "3.0.1"
@ -177,7 +206,7 @@ rev = "24.8.0"
[tool.poetry.dependencies.fsspec]
extras = [ "s3",]
version = "^2025.10.0"
version = "^2026.1.0"
[tool.poetry.dependencies.pysaml2]
git = "https://github.com/prauscher/pysaml2.git"

View file

@ -56,7 +56,7 @@ meteostat==1.6.8
beautifulsoup4==4.13.5
# pylint-odoo depend on pylint==3.3.9 https://github.com/OCA/pylint-odoo/pull/541
#pylint-odoo==9.3.22
pylint==3.3.9
#pylint==3.3.9
isort==6.1.0
# Fix some OS

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
Red='\033[0;31m' # Red
Color_Off='\033[0m' # Text Reset
# Argument 3 is config
if [[ $# -eq 3 ]]; then
./script/addons/check_addons_exist.py -m "$2" -c "$3"
else
./script/addons/check_addons_exist.py -m "$2"
fi
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo -e "${Red}Error${Color_Off} check_addons_exist.py into install_addons_debug.sh"
exit 1
fi
echo "Install module '$2' on BD '$1'"
# --log-level=debug for less debug information
if [[ $# -eq 3 ]]; then
./run.sh --no-http --stop-after-init --limit-memory-soft=8589934592 --limit-memory-hard=0 --dev cg -d "$1" -i "$2" -u "$2" -c "$3" --log-level=debug_sql --log-handler=odoo.modules.loading:DEBUG
# ./run.sh --no-http --stop-after-init fd --limit-memory-soft=2048*1024*1024 --dev cg -d "$1" -i "$2" -u "$2" -c "$3"
else
./run.sh --no-http --stop-after-init --limit-memory-soft=8589934592 --limit-memory-hard=0 --dev cg -d "$1" -i "$2" -u "$2" --log-level=debug_sql --log-handler=odoo.modules.loading:DEBUG
fi
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo -e "${Red}Error${Color_Off} run.sh into install_addons_debug.sh"
exit 1
fi

View file

@ -173,7 +173,7 @@ def fill_search_field(ast_obj, var_name="", py_filename=""):
elif ast_obj_type is ast.UnaryOp:
if type(ast_obj.op) is ast.USub:
# value is negative
result = ast_obj.operand.n * -1
result = ast_obj.operand.value * -1
else:
_logger.warning(
f"Cannot support keyword of variable {var_name} type"
@ -186,7 +186,7 @@ def fill_search_field(ast_obj, var_name="", py_filename=""):
# Support -> fields.Date.context_today
parent_node = ast_obj
lst_call_lambda = []
if hasattr(parent_node, "id"):
if hasattr(parent_node, "id") or hasattr(parent_node, "attr"):
while hasattr(parent_node, "value"):
lst_call_lambda.insert(0, parent_node.attr)
parent_node = parent_node.value
@ -200,16 +200,28 @@ def fill_search_field(ast_obj, var_name="", py_filename=""):
" 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]
result = [
fill_search_field(a, var_name=var_name, py_filename=py_filename)
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
fill_search_field(
k, var_name=var_name, py_filename=py_filename
): fill_search_field(
ast_obj.values[i], var_name=var_name, py_filename=py_filename
)
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])
result = tuple(
[
fill_search_field(
a, var_name=var_name, py_filename=py_filename
)
for a in ast_obj.elts
]
)
else:
_logger.warning(
f"Cannot support keyword of variable {var_name} type"
@ -321,8 +333,10 @@ def main():
dct_fields[var_name] = d
lst_args = []
for keyword in node.value.args:
value = _fill_search_field(
keyword, var_name
value = fill_search_field(
keyword,
var_name=var_name,
py_filename=str(py_file),
)
# Waste to stock None value
if value is not None:
@ -350,7 +364,9 @@ def main():
)
for keyword in node.value.keywords:
value = fill_search_field(
keyword.value, var_name
keyword.value,
var_name=var_name,
py_filename=str(py_file),
)
if value is not None:
d[keyword.arg] = value
@ -411,67 +427,6 @@ 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

@ -0,0 +1,97 @@
#!/usr/bin/env python3
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import argparse
import configparser
import getpass
import logging
import os
import sys
from subprocess import check_output
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
_logger = logging.getLogger(__name__)
def get_config():
"""Parse command line arguments, extracting the config file name,
returning the union of config file and command line arguments
:return: dict of config file settings and command line arguments
"""
# TODO update description
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
DESCRIPTION
Force config.conf file to has workers 0 and support queue
SUGGESTION
./script/config/setup_odoo_config_conf_devops.py
""",
epilog="""\
""",
)
args = parser.parse_args()
return args
def main():
config = get_config()
# check if it needs master password from config file
has_config_file = True
config_path = "./config.conf"
if not os.path.isfile(config_path):
config_path = "/etc/odoo/odoo.conf"
if not os.path.isfile(config_path):
has_config_file = False
if has_config_file:
config_parser = configparser.ConfigParser()
config_parser.read(config_path)
# Force workers to zero for local running
if config_parser.has_option("options", "workers"):
workers = config_parser.get("options", "workers")
if workers != "0":
config_parser.set("options", "workers", "0")
else:
config_parser.set("options", "workers", "0")
# Load queue_job
if config_parser.has_option("options", "server_wide_modules"):
server_wide_modules = config_parser.get(
"options", "server_wide_modules"
)
if "queue_job" not in server_wide_modules:
config_parser.set(
"options",
"server_wide_modules",
server_wide_modules + ",queue_job",
)
else:
config_parser.set(
"options", "server_wide_modules", "base,web,queue_job"
)
# Support queue_job size channels
queue_job_channels = ""
if config_parser.has_section("queue_job"):
if config_parser.has_option("options", "queue_job"):
queue_job_channels = config_parser.get("queue_job", "channels")
else:
config_parser.add_section("queue_job")
if not queue_job_channels:
config_parser.set(
"queue_job", "channels", f"root:{os.cpu_count()}"
)
with open(config_path, "w") as configfile:
config_parser.write(configfile)
if __name__ == "__main__":
main()

View file

@ -42,7 +42,10 @@ if [[ ${EL_MINIMAL_ADDONS} = "False" ]]; then
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_account-invoicing," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_account-payment," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_account-reconcile," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_agreement," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_ai," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_apps-store," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_automation," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_bank-payment," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_bank-payment-alternative," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_bank-statement-import," >> "${EL_CONFIG_FILE}"
@ -74,19 +77,28 @@ if [[ ${EL_MINIMAL_ADDONS} = "False" ]]; then
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_geospatial," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_helpdesk," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_hr," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_hr-attendance," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_hr-expense," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_hr-holidays," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_infrastructure," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_interface-github," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_intrastat-extrastat," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_iot," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_knowledge," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_l10n-canada," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_mail," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_maintenance," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_management-system," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_manufacture," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_manufacture-reporting," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_margin-analysis," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_mass-mailing," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_mis-builder," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_multi-company," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_odoo-pim," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_operating-unit," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_partner-contact," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_payroll," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_pos," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_product-attribute," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_product-configurator," >> "${EL_CONFIG_FILE}"
@ -95,10 +107,17 @@ if [[ ${EL_MINIMAL_ADDONS} = "False" ]]; then
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_project," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_project-agile," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_project-reporting," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_purchase-reporting," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_purchase-workflow," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_queue," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_repair," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_report-print-send," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_reporting-engine," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_rest-framework," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_rma," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_sale-blanket," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_sale-channel," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_sale-promotion," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_sale-reporting," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_sale-workflow," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_server-auth," >> "${EL_CONFIG_FILE}"
@ -107,6 +126,8 @@ if [[ ${EL_MINIMAL_ADDONS} = "False" ]]; then
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_server-env," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_server-tools," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_server-ux," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_shift-planning," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_shopfloor-app," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_sign," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_social," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_spreadsheet," >> "${EL_CONFIG_FILE}"
@ -120,17 +141,21 @@ if [[ ${EL_MINIMAL_ADDONS} = "False" ]]; then
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_vertical-isp," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_vertical-travel," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_web," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_web-api," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_website," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_website-cms," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OCA_wms," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/ONLYOFFICE_onlyoffice_odoo," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/OmniaGit_odooplm," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/TechnoLibre_odoo-code-generator," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/TechnoLibre_odoo-code-generator-template," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/bemade_bemade-addons," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/camptocamp_odoo-cloud-platform," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/cetmix_cetmix-tower," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/dhongu_deltatech," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/muk-it_odoo-modules," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/novacode-nl_odoo-formio," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/odoo_design-themes," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/odoomates_odooapps," >> "${EL_CONFIG_FILE}"
printf "${EL_HOME_ODOO_PROJECT}/addons/openeducat_openeducat_erp," >> "${EL_CONFIG_FILE}"
fi

View file

@ -14,6 +14,6 @@ sudo snap install core
sudo snap refresh core
# https://certbot.eff.org/lets-encrypt/debianbuster-nginx
# Cerbot
# Certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

View file

@ -14,6 +14,6 @@ sudo snap install core
sudo snap refresh core
# https://certbot.eff.org/lets-encrypt/debianbuster-nginx
# Cerbot
# Certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

View file

@ -0,0 +1,193 @@
#!/usr/bin/env python3
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import argparse
import sys
import psutil
STOP_PARENT_KILL = ["./odoo_bin.sh", "./run.sh"]
def proc_desc(p: psutil.Process) -> str:
try:
cmd = " ".join(p.cmdline()) if p.cmdline() else p.name()
return f"pid={p.pid} user={p.username()} name={p.name()} cmd={cmd}"
except psutil.Error:
return f"pid={p.pid}"
def get_ancestry(pid: int):
"""
Returns list of psutil.Process from child -> parent -> ... until PID 1 or missing.
"""
chain = []
try:
p = psutil.Process(pid)
except psutil.NoSuchProcess:
return chain
while True:
chain.append(p)
try:
ppid = p.ppid()
except psutil.Error:
break
if ppid <= 0 or ppid == p.pid:
break
try:
p = psutil.Process(ppid)
except psutil.NoSuchProcess:
break
if p.pid == 1:
chain.append(p)
break
return chain
def choose_target(chain, nb_parent):
"""
Decide which process to kill.
Default: kill the highest ancestor before PID 1 (so not systemd).
"""
if not chain:
return None
lst_chain_parent = []
for pid in chain:
lst_chain_parent.append(pid)
for str_to_stop in STOP_PARENT_KILL:
if str_to_stop in pid.cmdline():
return pid, lst_chain_parent
return chain[nb_parent - 1], [chain[nb_parent - 1]]
def kill_process(p: psutil.Process, force: bool):
if force:
p.kill()
else:
p.terminate()
def kill_tree(root: psutil.Process, force: bool):
"""
Kill root and all its children (best-effort).
"""
children = root.children(recursive=True)
# terminate children first
for ch in children:
try:
kill_process(ch, force=force)
except psutil.Error:
pass
try:
kill_process(root, force=force)
except psutil.Error:
pass
# wait a bit
gone, alive = psutil.wait_procs(children + [root], timeout=3)
return alive
def find_listeners(port: int):
pids = set()
for c in psutil.net_connections(kind="tcp"):
if (
c.laddr
and c.laddr.port == port
and c.status == psutil.CONN_LISTEN
and c.pid
):
pids.add(c.pid)
return sorted(pids)
def main():
ap = argparse.ArgumentParser(
description="Free a TCP port by killing the parent (or process tree) of the listener."
)
ap.add_argument("port", type=int, help="TCP port (1-65535)")
ap.add_argument(
"-n",
"--dry-run",
action="store_true",
help="Show what would be killed, do nothing",
)
ap.add_argument(
"-f",
"--force",
action="store_true",
help="Use SIGKILL instead of SIGTERM",
)
ap.add_argument(
"--kill-tree",
action="store_true",
help="Kill target and all its children recursively",
)
ap.add_argument(
"--nb_parent",
type=int,
default=1,
help="Kill parent too",
)
args = ap.parse_args()
if not (1 <= args.port <= 65535):
print("Port invalide (1-65535).", file=sys.stderr)
return 2
pids = find_listeners(args.port)
if not pids:
print(f"Aucun process en LISTEN sur {args.port}.")
return 1
for pid in pids:
chain = get_ancestry(pid)
if not chain:
continue
target, lst_target = choose_target(chain, args.nb_parent)
print(f"\nListener PID {pid} ancestry:")
for i, p in enumerate(chain):
prefix = " " + ("-> " if p == target else " ")
print(prefix + proc_desc(p))
if target is None:
continue
if target.pid == 1 and not args.allow_pid1:
print(
"Refus: la cible est PID 1 (systemd). Utilise plutôt systemctl pour arrêter le service.",
file=sys.stderr,
)
continue
action = "KILL" if args.force else "TERM"
if args.kill_tree:
print(f"Target: pid={target.pid} ({action}) + children (tree)")
if not args.dry_run:
alive = kill_tree(target, force=args.force)
if alive:
print(
"Toujours vivants:",
", ".join(str(p.pid) for p in alive),
file=sys.stderr,
)
else:
print(f"Target: pid={target.pid} ({action})")
if not args.dry_run:
try:
kill_process(target, force=args.force)
except psutil.AccessDenied as e:
print(
f"AccessDenied: {e} (lance le script avec sudo).",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -68,6 +68,8 @@ MONTHS_FR = {
"decembre": 12,
}
defaut_timeout = 60
class SeleniumLib(object):
def __init__(self, config):
@ -437,7 +439,7 @@ class SeleniumLib(object):
word = ""
return word
def click(self, xpath, timeout=5):
def click(self, xpath, timeout=defaut_timeout):
wait = WebDriverWait(self.driver, timeout)
button = wait.until(
EC.element_to_be_clickable(
@ -454,7 +456,7 @@ class SeleniumLib(object):
self,
by: str = By.ID,
value: str = None,
timeout=5,
timeout=defaut_timeout,
wait_clickable=False,
wait_is_invisible=False,
is_visible=True,
@ -503,7 +505,7 @@ class SeleniumLib(object):
return ele[0]
def get_element_not_visible(
self, by: str = By.ID, value: str = None, timeout=5
self, by: str = By.ID, value: str = None, timeout=defaut_timeout
):
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
@ -518,7 +520,7 @@ class SeleniumLib(object):
return ele
def get_elements_not_visible(
self, by: str = By.ID, value: str = None, timeout=5
self, by: str = By.ID, value: str = None, timeout=defaut_timeout
):
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
@ -533,12 +535,14 @@ class SeleniumLib(object):
return ele
def get_text_from_element(
self, by: str = By.ID, value: str = None, timeout=5
self, by: str = By.ID, value: str = None, timeout=defaut_timeout
):
ele = self.get_element(by, value, timeout=timeout)
return ele.text
def get_all_element(self, by: str = By.ID, value: str = None, timeout=5):
def get_all_element(
self, by: str = By.ID, value: str = None, timeout=defaut_timeout
):
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
EC.visibility_of_all_elements_located(
@ -556,7 +560,7 @@ class SeleniumLib(object):
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=5,
timeout=defaut_timeout,
inject_cursor=False,
):
new_element = self.wait_new_element(
@ -573,7 +577,7 @@ class SeleniumLib(object):
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=5,
timeout=defaut_timeout,
timeout_wait=None,
):
element = None
@ -588,7 +592,11 @@ class SeleniumLib(object):
return element
def wait_add_new_element_and_click(
self, by: str = By.ID, value: str = None, delay_wait=1, timeout=5
self,
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=defaut_timeout,
):
new_element = self.wait_add_new_element(
by=by, value=value, delay_wait=delay_wait, timeout=timeout
@ -597,7 +605,11 @@ class SeleniumLib(object):
return new_element
def wait_add_new_element(
self, by: str = By.ID, value: str = None, delay_wait=1, timeout=5
self,
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=defaut_timeout,
):
all_element_init = self.get_all_element(by=by, value=value)
len_all_element_init = len(all_element_init)
@ -612,7 +624,11 @@ class SeleniumLib(object):
return new_element
def wait_increment_number_text_and_click(
self, by: str = By.ID, value: str = None, delay_wait=1, timeout=5
self,
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=defaut_timeout,
):
element_init = self.get_element(by=by, value=value, timeout=timeout)
actual_number = int(element_init.text)
@ -631,7 +647,7 @@ class SeleniumLib(object):
self,
by: str = By.ID,
value: str = None,
timeout: int = 5,
timeout: int = defaut_timeout,
no_scroll: bool = False,
viewport_ele_by: str = By.ID,
viewport_ele_value: str = None,
@ -696,7 +712,7 @@ class SeleniumLib(object):
self,
by: str = By.ID,
value: str = None,
timeout: int = 5,
timeout: int = defaut_timeout,
form="carre",
):
@ -720,7 +736,7 @@ class SeleniumLib(object):
value: str = None,
text_value: str = "",
delay_after: int = -1,
timeout: int = 5,
timeout: int = defaut_timeout,
no_scroll: bool = False,
viewport_ele_by: str = By.ID,
viewport_ele_value: str = None,
@ -1093,7 +1109,7 @@ class SeleniumLib(object):
" odoo_website_menu_click"
)
button = self.click_with_mouse_move(
By.LINK_TEXT, from_text, timeout=30
By.LINK_TEXT, from_text, timeout=defaut_timeout
)
return button
@ -1113,11 +1129,10 @@ class SeleniumLib(object):
By.XPATH,
"//button[contains(@class, 'dropdown-toggle') and"
" .//i[contains(@class, 'oi-apps')]]",
timeout=10,
)
elif self.odoo_version == "14.0":
button = self.click_with_mouse_move(
By.CSS_SELECTOR, "a.full[data-toggle='dropdown']", timeout=30
By.CSS_SELECTOR, "a.full[data-toggle='dropdown']"
)
return button
@ -1126,7 +1141,6 @@ class SeleniumLib(object):
all_row = self.get_all_element(
by=By.XPATH,
value=f"//tr[contains(@class, 'o_data_row')]/td[contains(@name, '{field_name}')]",
timeout=10,
)
row = all_row[index] if index < len(all_row) else all_row[-1]
button = self.click_with_mouse_move(element=row)
@ -1137,7 +1151,6 @@ class SeleniumLib(object):
button = self.click_with_mouse_move(
By.XPATH,
"//button[contains(@class, 'o_searchview_dropdown_toggler')]",
timeout=10,
)
return button
@ -1146,14 +1159,12 @@ class SeleniumLib(object):
button = self.click_with_mouse_move(
By.XPATH,
value="//select[contains(@class, 'o_add_custom_group_menu')]",
timeout=10,
)
if self.wait_human_time:
time.sleep(1)
select_elem = self.get_element(
By.XPATH,
value="//select[contains(@class, 'o_add_custom_group_menu')]",
timeout=10,
wait_clickable=False,
)
@ -1174,20 +1185,17 @@ class SeleniumLib(object):
button = self.click_with_mouse_move(
By.XPATH,
value=f"//button[contains(@class,'o_menu_item') and contains(normalize-space(.), '{filter_label}')]",
timeout=10,
)
if filter_sub_label:
button = self.click_with_mouse_move(
By.XPATH,
value=f"//span[contains(@class,'o_item_option') and contains(normalize-space(.), '{filter_sub_label}')]",
timeout=10,
)
if lst_update_condition:
filter_text = f"{filter_label}: {filter_sub_label}"
button = self.click_with_mouse_move(
By.XPATH,
value=f"//small[contains(@class,'o_facet_value') and contains(normalize-space(.), '{filter_text}')]/../..//i[contains(@class,'fa-cog')]",
timeout=10,
is_visible=False,
)
time.sleep(1)
@ -1197,7 +1205,6 @@ class SeleniumLib(object):
select_elem = self.click_with_mouse_move(
By.XPATH,
value=f"//div[contains(@class,'modal-content')]//select[contains(@class,'pe-3')]",
timeout=10,
is_visible=False,
)
select_obj = Select(select_elem)
@ -1211,14 +1218,12 @@ class SeleniumLib(object):
button = self.click_with_mouse_move(
By.XPATH,
value=f"//footer[contains(@class,'modal-footer')]//button[contains(@class,'btn-primary') and contains(normalize-space(.), 'Confirmer')]",
timeout=10,
)
def odoo_fill_search_input(self, text, selection_research=None):
input_elem = self.click_with_mouse_move(
By.XPATH,
value="//input[contains(@class, 'o_searchview_input')]",
timeout=10,
)
input_elem.send_keys(text)
time.sleep(1)
@ -1240,15 +1245,13 @@ class SeleniumLib(object):
"Cannot click, empty parameter from method"
" odoo_web_principal_menu_click_root_menu"
)
button = self.click_with_mouse_move(
By.LINK_TEXT, from_text, timeout=30
)
button = self.click_with_mouse_move(By.LINK_TEXT, from_text)
# If not work, force it
# selenium_tool.driver.execute_script("arguments[0].click();", button)
return button
def odoo_web_form_click_statusbar_button_status(
self, status_label, timeout=10
self, status_label, timeout=defaut_timeout
):
try:
status_button = self.click_with_mouse_move(
@ -1294,7 +1297,7 @@ class SeleniumLib(object):
toggle_data_menu("color_scheme.switch", enable_dark_mode)
def odoo_web_form_click_statusbar_button_status_floating(
self, status_label, timeout=10
self, status_label, timeout=defaut_timeout
):
status_button = self.click_with_mouse_move(
By.XPATH,
@ -1306,7 +1309,7 @@ class SeleniumLib(object):
return status_button
def odoo_web_form_click_statusbar_button_status_plus(
self, status_label, timeout=10
self, status_label, timeout=defaut_timeout
):
status_button = self.click_with_mouse_move(
By.XPATH,
@ -1324,7 +1327,6 @@ class SeleniumLib(object):
By.XPATH,
f"//button[contains(@class, '{btn_class}') and contains(span,"
f" '{btn_label}')]",
timeout=30,
)
print(f"Bouton du statusbar avec le label '{btn_label}' cliqué.")
return status_button
@ -1333,7 +1335,6 @@ class SeleniumLib(object):
status_button = self.click_with_mouse_move(
By.NAME,
btn_action,
timeout=30,
)
print(f"Bouton avec l'action '{btn_action}' cliqué.")
return status_button
@ -1343,9 +1344,7 @@ class SeleniumLib(object):
"o_form_button_save"
)
def odoo_web_change_view(
self, view_name="list", raise_error=False, timeout=5
):
def odoo_web_change_view(self, view_name="list", raise_error=False):
if view_name not in ["list", "kanban", "timeline"]:
msg = f"odoo_web_change_view view_name '{view_name}' invalid"
if raise_error:
@ -1367,7 +1366,6 @@ class SeleniumLib(object):
status_button = self.click_with_mouse_move(
By.CLASS_NAME,
custom_class_name,
timeout=30,
)
print(f"Bouton avec l'action '{custom_class_name}' cliqué.")
return status_button
@ -1378,7 +1376,6 @@ class SeleniumLib(object):
By.XPATH,
"//div[contains(@class,"
f" 'o_kanban_record')]//span[contains(text(), '{card_label}')]",
timeout=30,
)
# kanban_card = self.driver.find_element(By.XPATH,
# f"//div[contains(@class, 'o_kanban_record')]//span[contains(text(), '{card_label}')]")
@ -1388,9 +1385,7 @@ class SeleniumLib(object):
def odoo_web_action_button_new(self):
# From web view kanban, list
return self.click_with_mouse_move(
By.CLASS_NAME, "o-kanban-button-new", timeout=30
)
return self.click_with_mouse_move(By.CLASS_NAME, "o-kanban-button-new")
def odoo_web_kanban_click(self, button_text, btn_class="btn-primary"):
# From web view kanban, list
@ -1398,14 +1393,12 @@ class SeleniumLib(object):
By.XPATH,
f"//button[contains(@class, 'btn {btn_class} o_kanban_edit') and"
f" text()='{button_text}']",
timeout=30,
)
def odoo_web_form_notebook_tab_click(self, tab_text):
return self.click_with_mouse_move(
By.XPATH,
f"//a[text()='{tab_text}']",
timeout=30,
)
# Web

View file

@ -2,6 +2,7 @@
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import configparser
import datetime
import getpass
import json
@ -11,6 +12,7 @@ import shutil
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
import zipfile
new_path = os.path.normpath(
@ -29,6 +31,13 @@ INSTALLED_ODOO_VERSION_FILE = os.path.join(
ODOO_VERSION_FILE = ".odoo-version"
ENABLE_CRASH = False
CRASH_E = None
# Support mobile ERPLibre
ANDROID_DIR = "android"
MOBILE_HOME_PATH = "./mobile/erplibre_home_mobile"
STRINGS_FILE = os.path.join(
MOBILE_HOME_PATH, ANDROID_DIR, "app/src/main/res/values/strings.xml"
)
GRADLE_FILE = os.path.join(MOBILE_HOME_PATH, ANDROID_DIR, "app/build.gradle")
try:
import tkinter as tk
@ -66,7 +75,6 @@ _logger = logging.getLogger(__name__)
CONFIG_FILE = "./script/todo/todo.json"
CONFIG_OVERRIDE_FILE = "./private/todo/todo.json"
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
PATH_MOBILE_PROJECT_HOME = "./mobile/technolibre_home_mobile/technolibre_home"
class TODO:
@ -220,6 +228,7 @@ class TODO:
[4] Code - Outil pour développeur
[5] Doc - Recherche de documentation
[6] Database - Outils sur les bases de données
[7] Process - Outils sur les exécutions
[0] Retour
"""
while True:
@ -251,6 +260,10 @@ class TODO:
status = self.prompt_execute_database()
if status is not False:
return
elif status == "7":
status = self.prompt_execute_process()
if status is not False:
return
else:
print("Commande non trouvée 🤖!")
@ -463,7 +476,7 @@ class TODO:
lst_instance = self.config_file.get_config("instance")
init_len = len(lst_instance)
if os.path.exists(PATH_MOBILE_PROJECT_HOME):
if os.path.exists(MOBILE_HOME_PATH):
dct_upgrade_odoo_database = {
"prompt_description": "Mobile - Compile and run software",
"callback": self.callback_make_mobile_home,
@ -681,6 +694,23 @@ class TODO:
else:
print("Commande non trouvée 🤖!")
def prompt_execute_process(self):
print("🤖 Manipuler les processus d'exécution!")
lst_instance = [
{"prompt_description": "Kill process from actual port"},
]
help_info = self.fill_help_info(lst_instance)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.process_kill_from_port()
else:
print("Commande non trouvée 🤖!")
def get_odoo_version(self):
with open(VERSION_DATA_FILE) as txt:
data_version = json.load(txt)
@ -826,7 +856,7 @@ class TODO:
commande = (
f"source ./.venv.{source_odoo}/bin/activate && {commande}"
)
elif new_window:
elif new_window and self.cmd_source_default:
commande = self.cmd_source_default % commande
print("🏠 ⬇ Execute command :")
@ -1056,6 +1086,16 @@ class TODO:
source_erplibre=False,
)
def process_kill_from_port(self):
cfg = configparser.ConfigParser()
cfg.read("./config.conf")
http_port = cfg.getint("options", "http_port")
status = self.executer_commande_live(
f"./script/process/kill_process_by_port.py {http_port} --kill-tree --nb_parent 2",
source_erplibre=False,
)
def download_database_backup_cli(self, show_remote_list=True):
database_domain = input("Domain Odoo (ex. https://mondomain.com) : ")
if show_remote_list:
@ -1147,12 +1187,38 @@ class TODO:
todo_file_browser.exit_program()
def callback_make_mobile_home(self, dct_config):
# Read file
default_project_name = "ERPLibre"
default_package_name = "ca.erplibre.home"
# Read default information
if os.path.exists(STRINGS_FILE):
tree = ET.parse(STRINGS_FILE)
root = tree.getroot()
for elem in root.findall("string"):
if elem.get("name") == "app_name":
default_project_name = elem.text
if elem.get("name") == "package_name":
default_package_name = elem.text
default_project_url_name = "https://erplibre.ca"
# Read default information
dotenv_file = dotenv.find_dotenv(
filename=os.path.join(MOBILE_HOME_PATH, "src", ".env.production")
)
default_project_url_name = dotenv.get_key(
dotenv_file, "VITE_WEBSITE_URL"
)
default_project_note_subject = dotenv.get_key(
dotenv_file, "VITE_LABEL_NOTE"
)
default_debug = False
project_name = default_project_name
project_url_name = default_project_url_name
project_principal_subject = default_project_note_subject
package_name = default_package_name
do_debug = default_debug
do_change_picture_menu = False
do_personalize = input(
"Do you want to personalize the mobile application (Y) : "
@ -1160,26 +1226,47 @@ class TODO:
if do_personalize.strip().lower() == "y":
project_name = (
input(
f"Your project name, default {default_project_name} : "
f'Your project name (Separate by space in title), default "{default_project_name}" : '
).strip()
or default_project_name
)
package_name = (
input(
f'Your package name (separate by . lower case, 3 works like DOMAIN.NAME.OBJECT), default "{default_package_name}" : '
).strip()
or default_package_name
)
project_url_name = (
input(
f"Your project url website, default {default_project_url_name} : "
f'Your project url website, default "{default_project_url_name}" : '
).strip()
or default_project_url_name
)
project_principal_subject = (
input(
f'Your project subject, default "{default_project_note_subject}" : '
).strip()
or default_project_note_subject
)
do_debug = (
input("Do you want debug information (Y) :").strip().lower()
input("Compilation with debug information, default No (Y) : ")
.strip()
.lower()
== "y"
)
do_change_picture_menu = (
input(
"Want to change picture from menu, you need android-studio (Y) : "
)
.strip()
.lower()
== "y"
)
dotenv_file = dotenv.find_dotenv(
filename=os.path.join(
PATH_MOBILE_PROJECT_HOME, "src", ".env.production"
)
)
# Rename with script bash
cmd_client = f'cd {MOBILE_HOME_PATH} && npx cap init "{project_name}" "{package_name}" && ./rename_android.sh "{project_name}" "{package_name}" && npx cap sync android'
self.executer_commande_live(cmd_client, source_erplibre=False)
# dotenv_mobile = dotenv.dotenv_values(dotenv_file)
# dotenv_mobile["VITE_TITLE"] = project_name
# dotenv_mobile["VITE_WEBSITE_URL"] = project_url_name
@ -1192,12 +1279,36 @@ class TODO:
project_url_name,
quote_mode="always",
)
dotenv.set_key(
dotenv_file,
"VITE_LABEL_NOTE",
project_principal_subject,
quote_mode="always",
)
dotenv.set_key(
dotenv_file,
"VITE_DEBUG_DEV",
"true" if do_debug else "false",
quote_mode="never",
)
if do_change_picture_menu:
status = self.executer_commande_live(
f"cd {MOBILE_HOME_PATH} && npx cap open android;bash",
source_erplibre=False,
new_window=True,
)
print(
"Guide for Android-Studio, wait loading is finish. Right-click to app/New/Image Asset and load your image."
)
input(
"Did you finish to update image with Android-Studio ? Press to continue ..."
)
cmd_client = "cp ./mobile/erplibre_home_mobile/android/app/src/main/ic_launcher-playstore.png ./mobile/erplibre_home_mobile/src/assets/company_logo.png"
self.executer_commande_live(cmd_client, source_erplibre=False)
cmd_client = "cp ./mobile/erplibre_home_mobile/android/app/src/main/ic_launcher-playstore.png ./mobile/erplibre_home_mobile/src/assets/imgs/logo.png"
self.executer_commande_live(cmd_client, source_erplibre=False)
status = self.executer_commande_live(
"./mobile/compile_and_run.sh", source_erplibre=False
)

View file

@ -731,15 +731,24 @@ class TodoUpgrade:
if not self.dct_progression.get("state_1_restore_database"):
file_name = os.path.basename(self.file_path)
image_db_file_path = os.path.join("image_db", file_name)
if os.path.exists(image_db_file_path):
if not shutil._samefile(self.file_path, image_db_file_path):
str_will_copy = (
f"🤖 will copy '{self.file_path}' to '{image_db_file_path}'"
)
if not shutil._samefile(self.file_path, image_db_file_path):
do_copy = False
if os.path.exists(image_db_file_path):
status_overwrite_image_db = input(
f"🤖 will copy '{self.file_path}' to '{image_db_file_path}', "
f"{str_will_copy}, "
f"a file already exist, do you want to continue (y/Y) : "
).strip()
if status_overwrite_image_db.lower() != "y":
return
os.remove(image_db_file_path)
if status_overwrite_image_db.lower() == "y":
do_copy = True
os.remove(image_db_file_path)
else:
print(str_will_copy)
do_copy = True
if do_copy:
shutil.copy(self.file_path, image_db_file_path)
status, cmd_executed = self.todo_upgrade_execute(