[IMP] todo support odoo upgrade
- add example odoo test - prevent delete production file with validation - add makefile with selenium - script prod to dev uninstall module after installation - adapt todo with private directory - script to download remote database - TODO show documentation for migration
This commit is contained in:
parent
93617b1a65
commit
cbc43fde3c
31 changed files with 3641 additions and 202 deletions
11
Makefile
11
Makefile
|
|
@ -130,6 +130,10 @@ open_selenium:
|
||||||
############
|
############
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
format:
|
format:
|
||||||
|
./script/maintenance/format_file_to_commit.py
|
||||||
|
|
||||||
|
.PHONY: format_all
|
||||||
|
format_all:
|
||||||
parallel ::: "./script/make.sh format_code_generator" "./script/make.sh format_code_generator_template" "./script/make.sh format_script" "./script/make.sh format_erplibre_addons" "./script/make.sh format_supported_addons"
|
parallel ::: "./script/make.sh format_code_generator" "./script/make.sh format_code_generator_template" "./script/make.sh format_script" "./script/make.sh format_erplibre_addons" "./script/make.sh format_supported_addons"
|
||||||
|
|
||||||
.PHONY: format_code_generator
|
.PHONY: format_code_generator
|
||||||
|
|
@ -212,6 +216,11 @@ repo_configure_group_code_generator:
|
||||||
repo_show_status:
|
repo_show_status:
|
||||||
.venv.erplibre/bin/repo forall -pc "git status -s"
|
.venv.erplibre/bin/repo forall -pc "git status -s"
|
||||||
|
|
||||||
|
# Show git stash for all repo
|
||||||
|
.PHONY: repo_do_stash
|
||||||
|
repo_do_stash:
|
||||||
|
.venv.erplibre/bin/repo forall -pc "git stash"
|
||||||
|
|
||||||
# Show divergence between actual repository and production manifest
|
# Show divergence between actual repository and production manifest
|
||||||
.PHONY: repo_diff_manifest_production
|
.PHONY: repo_diff_manifest_production
|
||||||
repo_diff_manifest_production:
|
repo_diff_manifest_production:
|
||||||
|
|
@ -299,7 +308,7 @@ i18n_generate_demo_portal:
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete
|
find . \( -name '__pycache__' -type d -prune -o -name '*.pyc' -o -name '*.pyo' \) -exec rm -rf {} +
|
||||||
|
|
||||||
###############
|
###############
|
||||||
# Statistic #
|
# Statistic #
|
||||||
|
|
|
||||||
|
|
@ -107,4 +107,8 @@ docker_build_release_beta:
|
||||||
# docker clean all
|
# docker clean all
|
||||||
.PHONY: docker_clean_all
|
.PHONY: docker_clean_all
|
||||||
docker_clean_all:
|
docker_clean_all:
|
||||||
docker system prune -a --volumes
|
./script/terminal/validate_to_continue.sh "⚠️ This will REMOVE unused images, containers, networks, and VOLUMES." && docker system prune -a --volumes
|
||||||
|
|
||||||
|
.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
|
||||||
|
|
|
||||||
|
|
@ -119,3 +119,9 @@ test_addons_helpdesk:
|
||||||
./.venv.erplibre/bin/coverage report -m
|
./.venv.erplibre/bin/coverage report -m
|
||||||
./.venv.erplibre/bin/coverage html
|
./.venv.erplibre/bin/coverage html
|
||||||
./.venv.erplibre/bin/coverage json
|
./.venv.erplibre/bin/coverage json
|
||||||
|
|
||||||
|
.PHONY: test_addons_project_sale_link_specific_file
|
||||||
|
test_addons_project_sale_link_specific_file:
|
||||||
|
./odoo_bin.sh db --drop --database test_addons_project_sale_link
|
||||||
|
./script/addons/install_addons_dev.sh test_addons_project_sale_link project_sale_link
|
||||||
|
./test.sh -d test_addons_project_sale_link --db-filter test_addons_project_sale_link -i project_sale_link --test-file=odoo18.0/addons/OCA_project/project_sale_link/tests/test_project_sale_link.py
|
||||||
|
|
|
||||||
16
doc/FAQ.md
16
doc/FAQ.md
|
|
@ -1,5 +1,18 @@
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
|
## Into execution
|
||||||
|
|
||||||
|
### wkthmltopdf TimeoutError
|
||||||
|
|
||||||
|
If you find this bug on server log :
|
||||||
|
`odoo.addons.base.models.ir_actions_report: wkhtmltopdf: Exit with code 1 due to network error: TimeoutError`
|
||||||
|
|
||||||
|
Into configuration, technique, go to ir.config_parameter (system parameter) and add configuration :
|
||||||
|
```
|
||||||
|
key : report.url
|
||||||
|
value : http://127.0.0.1:8069
|
||||||
|
```
|
||||||
|
|
||||||
## Scripting
|
## Scripting
|
||||||
|
|
||||||
### Search all duplicate file recursively in given directory
|
### Search all duplicate file recursively in given directory
|
||||||
|
|
@ -188,7 +201,8 @@ limit_memory_hard = 0
|
||||||
### Error non-overlapping IPv4 address pool
|
### Error non-overlapping IPv4 address pool
|
||||||
|
|
||||||
You got this error when you start a
|
You got this error when you start a
|
||||||
docker-compose: `ERROR: could not find an available, non-overlapping IPv4 address pool among the defaults to assign to the network`
|
docker-compose:
|
||||||
|
`ERROR: could not find an available, non-overlapping IPv4 address pool among the defaults to assign to the network`
|
||||||
|
|
||||||
It's because the subnet is limited, you need to change it.
|
It's because the subnet is limited, you need to change it.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ services:
|
||||||
# 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
|
- ./addons/addons:/ERPLibre/odoo18.0/addons/addons
|
||||||
|
- ./private:/ERPLibre/private
|
||||||
- erplibre_conf:/etc/odoo
|
- erplibre_conf:/etc/odoo
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
|
|
||||||
91
docker/docker-compose_odoo_18.0_selenium.yml
Normal file
91
docker/docker-compose_odoo_18.0_selenium.yml
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
services:
|
||||||
|
ERPLibre:
|
||||||
|
image: technolibre/erplibre:1.6.0_odoo_18.0_4f4f563
|
||||||
|
ports:
|
||||||
|
- 8069:8069
|
||||||
|
- 8071:8071
|
||||||
|
- 8072:8072
|
||||||
|
environment:
|
||||||
|
HOST: db,selenium-hub
|
||||||
|
PASSWORD: mysecretpassword
|
||||||
|
USER: odoo
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
STOP_BEFORE_INIT: "False"
|
||||||
|
DB_NAME: ""
|
||||||
|
UPDATE_ALL_DB: "False"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- selenium-hub
|
||||||
|
# not behind a proxy
|
||||||
|
#command: odoo --workers 0
|
||||||
|
# behind a proxy
|
||||||
|
#command: odoo --workers 2 --proxy-mode
|
||||||
|
# For production ready
|
||||||
|
#command: odoo --workers 2 --proxy-mode --no-database-list
|
||||||
|
# Increase max memory
|
||||||
|
#command: odoo --limit-memory-hard=0 --limit-memory-soft=0
|
||||||
|
command: odoo
|
||||||
|
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
|
||||||
|
- ./private:/ERPLibre/private
|
||||||
|
- erplibre_conf:/etc/odoo
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgis/postgis:18-3.6-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: mysecretpassword
|
||||||
|
POSTGRES_USER: odoo
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
PGDATA: /var/lib/postgresql/pgdata
|
||||||
|
volumes:
|
||||||
|
- erplibre-db-data:/var/lib/postgresql/pgdata
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
chrome:
|
||||||
|
image: selenium/node-chrome:4.34.0-20250707
|
||||||
|
platform: linux/amd64
|
||||||
|
shm_size: 2gb
|
||||||
|
depends_on:
|
||||||
|
- selenium-hub
|
||||||
|
environment:
|
||||||
|
- SE_EVENT_BUS_HOST=selenium-hub
|
||||||
|
|
||||||
|
edge:
|
||||||
|
image: selenium/node-edge:4.34.0-20250707
|
||||||
|
platform: linux/amd64
|
||||||
|
shm_size: 2gb
|
||||||
|
depends_on:
|
||||||
|
- selenium-hub
|
||||||
|
environment:
|
||||||
|
- SE_EVENT_BUS_HOST=selenium-hub
|
||||||
|
|
||||||
|
firefox:
|
||||||
|
image: selenium/node-firefox:4.34.0-20250707
|
||||||
|
shm_size: 2gb
|
||||||
|
depends_on:
|
||||||
|
- selenium-hub
|
||||||
|
environment:
|
||||||
|
- SE_EVENT_BUS_HOST=selenium-hub
|
||||||
|
|
||||||
|
# put into config.conf selenium_network_url=http://selenium-hub:4444
|
||||||
|
# in python
|
||||||
|
# from odoo.tools import config
|
||||||
|
# config.get("selenium_network_url")
|
||||||
|
selenium-hub:
|
||||||
|
image: selenium/hub:4.34.0-20250707
|
||||||
|
container_name: selenium-hub
|
||||||
|
ports:
|
||||||
|
- "8073:4442"
|
||||||
|
- "8074:4443"
|
||||||
|
- "8075:4444"
|
||||||
|
|
||||||
|
# We configure volume without specific destination to let docker manage it. To configure it through docker use (read related documentation before continuing) :
|
||||||
|
# - docker volume --help
|
||||||
|
# - docker-compose down --help
|
||||||
|
volumes:
|
||||||
|
erplibre_data_dir:
|
||||||
|
erplibre_conf:
|
||||||
|
erplibre-db-data:
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import configparser
|
import configparser
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -52,6 +53,16 @@ def get_config():
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Print path if module exist",
|
help="Print path if module exist",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output_json",
|
||||||
|
action="store_true",
|
||||||
|
help="output json for automation",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--format_json",
|
||||||
|
action="store_true",
|
||||||
|
help="output formated json",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
@ -60,6 +71,8 @@ def main():
|
||||||
config = get_config()
|
config = get_config()
|
||||||
if config.debug:
|
if config.debug:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
if not config.output_path and config.output_json:
|
||||||
|
config.output_path = True
|
||||||
|
|
||||||
config_parser = configparser.ConfigParser()
|
config_parser = configparser.ConfigParser()
|
||||||
config_parser.read(config.config)
|
config_parser.read(config.config)
|
||||||
|
|
@ -67,21 +80,30 @@ def main():
|
||||||
if "addons_path" in config_parser["options"]:
|
if "addons_path" in config_parser["options"]:
|
||||||
addons_path = config_parser["options"]["addons_path"]
|
addons_path = config_parser["options"]["addons_path"]
|
||||||
else:
|
else:
|
||||||
_logger.error(
|
msg = f"Missing item 'addons_path' in section 'options' in '{config.config}'"
|
||||||
"Missing item 'addons_path' in section 'options' in"
|
if not config.output_json:
|
||||||
f" '{config.config}'"
|
_logger.error(msg)
|
||||||
)
|
else:
|
||||||
|
print("{'error':%s}" % msg)
|
||||||
return -1
|
return -1
|
||||||
else:
|
else:
|
||||||
_logger.error(f"Missing section 'options' in '{config.config}'")
|
msg = f"Missing section 'options' in '{config.config}'"
|
||||||
|
if not config.output_json:
|
||||||
|
_logger.error(msg)
|
||||||
|
else:
|
||||||
|
print("{'error':%s}" % msg)
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
lst_addons_path = addons_path.strip(",").split(",")
|
lst_addons_path = addons_path.strip(",").split(",")
|
||||||
lst_module = config.module.strip(",").split(",")
|
lst_module = sorted(list(set(config.module.strip(",").split(","))))
|
||||||
|
|
||||||
dct_module_exist = defaultdict(list)
|
dct_module_exist = defaultdict(list)
|
||||||
dct_module_exist_empty = defaultdict(list)
|
dct_module_exist_empty = defaultdict(list)
|
||||||
lst_module_not_exist = []
|
lst_module_not_exist = []
|
||||||
|
lst_error = []
|
||||||
|
lst_exist = []
|
||||||
|
lst_missing = []
|
||||||
|
lst_duplicate = []
|
||||||
|
|
||||||
for module in lst_module:
|
for module in lst_module:
|
||||||
for path in lst_addons_path:
|
for path in lst_addons_path:
|
||||||
|
|
@ -102,10 +124,11 @@ def main():
|
||||||
is_good = False
|
is_good = False
|
||||||
error_missing_module = True
|
error_missing_module = True
|
||||||
module_list = "'" + "', '".join(lst_module_not_exist) + "'"
|
module_list = "'" + "', '".join(lst_module_not_exist) + "'"
|
||||||
_logger.error(
|
msg = f"Missing module{'s' if len(lst_module_not_exist) > 1 else ''} {module_list}"
|
||||||
"Missing"
|
if not config.output_json:
|
||||||
f" module{'s' if len(lst_module_not_exist) > 1 else ''} {module_list}"
|
_logger.error(msg)
|
||||||
)
|
else:
|
||||||
|
lst_missing.extend(lst_module_not_exist)
|
||||||
if dct_module_exist:
|
if dct_module_exist:
|
||||||
for key, lst_value in dct_module_exist.items():
|
for key, lst_value in dct_module_exist.items():
|
||||||
is_print_value = False
|
is_print_value = False
|
||||||
|
|
@ -113,20 +136,41 @@ def main():
|
||||||
is_print_value = True
|
is_print_value = True
|
||||||
is_good = False
|
is_good = False
|
||||||
module_list = "'" + "', '".join(lst_value) + "'"
|
module_list = "'" + "', '".join(lst_value) + "'"
|
||||||
_logger.error(f"Conflict modules: {module_list}")
|
msg = f"Conflict modules: {module_list}"
|
||||||
|
if not config.output_json:
|
||||||
|
_logger.error(msg)
|
||||||
|
else:
|
||||||
|
lst_duplicate.append((key, lst_value))
|
||||||
elif lst_value and config.output_path:
|
elif lst_value and config.output_path:
|
||||||
is_print_value = True
|
is_print_value = True
|
||||||
if is_print_value:
|
if is_print_value:
|
||||||
for value in lst_value:
|
for value in lst_value:
|
||||||
print(value)
|
if len(lst_value) == 1:
|
||||||
|
lst_exist.append((key, value))
|
||||||
|
if not config.output_json:
|
||||||
|
print(value)
|
||||||
|
|
||||||
if dct_module_exist_empty and not config.output_path:
|
if dct_module_exist_empty and not config.output_path:
|
||||||
for key, lst_value in dct_module_exist_empty.items():
|
for key, lst_value in dct_module_exist_empty.items():
|
||||||
module_list = "'" + "', '".join(lst_value) + "'"
|
module_list = "'" + "', '".join(lst_value) + "'"
|
||||||
_logger.warning(
|
msg = f"Found this directory, but missing __manifest__.py: {module_list}"
|
||||||
"Found this directory, but missing __manifest__.py:"
|
if not config.output_json:
|
||||||
f" {module_list}"
|
_logger.warning(msg)
|
||||||
)
|
else:
|
||||||
|
lst_error.append(msg)
|
||||||
|
|
||||||
|
if config.output_json:
|
||||||
|
dct_json_data = {
|
||||||
|
"exist": lst_exist,
|
||||||
|
"error": lst_error,
|
||||||
|
"duplicate": lst_duplicate,
|
||||||
|
"missing": lst_missing,
|
||||||
|
}
|
||||||
|
if config.format_json:
|
||||||
|
json_data = json.dumps(dct_json_data, indent=4, sort_keys=True)
|
||||||
|
else:
|
||||||
|
json_data = json.dumps(dct_json_data)
|
||||||
|
print(json_data)
|
||||||
|
|
||||||
if not is_good:
|
if not is_good:
|
||||||
if error_missing_module:
|
if error_missing_module:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
Red='\033[0;31m' # Red
|
Red='\033[0;31m' # Red
|
||||||
Color_Off='\033[0m' # Text Reset
|
Color_Off='\033[0m' # Text Reset
|
||||||
|
|
||||||
# This script will remove mail configuration, remove backup configuration, and force admin user to test/test
|
# This script will remove mail configuration, remove backup configuration, and force admin user to test/test
|
||||||
echo "Update prod to dev on BD '$1'"
|
echo "Update prod to dev on BD '$1'"
|
||||||
|
|
@ -12,3 +12,13 @@ if [[ $retVal -ne 0 ]]; then
|
||||||
echo -e "${Red}Error${Color_Off} install_addons.sh into update_prod_to_dev.sh"
|
echo -e "${Red}Error${Color_Off} install_addons.sh into update_prod_to_dev.sh"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Update trace of prod to dev on BD '$1'"
|
||||||
|
|
||||||
|
./script/addons/uninstall_addons.sh "$1" user_test,disable_mail_server,disable_auto_backup
|
||||||
|
|
||||||
|
retVal=$?
|
||||||
|
if [[ $retVal -ne 0 ]]; then
|
||||||
|
echo -e "${Red}Error${Color_Off} uninstall_addons.sh into update_prod_to_dev.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
|
||||||
113
script/code/git_commit_migration_addons_path.py
Executable file
113
script/code/git_commit_migration_addons_path.py
Executable file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
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="""\
|
||||||
|
|
||||||
|
""",
|
||||||
|
epilog="""\
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--path",
|
||||||
|
required=True,
|
||||||
|
help="Path addons to commit each module for migration",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--odoo_version",
|
||||||
|
required=True,
|
||||||
|
help="The version name to update for commit, example 12.0",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def run_git_command(command, ignore_error=False):
|
||||||
|
"""Exécute une commande Git et gère les erreurs."""
|
||||||
|
result = subprocess.run(
|
||||||
|
command, capture_output=True, text=True, shell=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0 and not ignore_error:
|
||||||
|
print(f"Error executing command: {' '.join(command)}")
|
||||||
|
print(result.stderr)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def commit_by_directory():
|
||||||
|
"""Crée un commit pour chaque répertoire contenant des fichiers modifiés."""
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
# Exécuter 'git status --porcelain' pour obtenir les fichiers modifiés
|
||||||
|
status_result = run_git_command(
|
||||||
|
f'cd "{config.path}" && git status --porcelain'
|
||||||
|
)
|
||||||
|
if status_result.returncode != 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Stocker les répertoires uniques
|
||||||
|
modified_dirs = set()
|
||||||
|
for line in status_result.stdout.splitlines():
|
||||||
|
# Extrait le chemin du fichier
|
||||||
|
file_path = line[3:].strip()
|
||||||
|
# Récupère le nom du répertoire et l'ajoute au set
|
||||||
|
dir_name = os.path.dirname(file_path)
|
||||||
|
if dir_name: # Assure que ce n'est pas le répertoire racine
|
||||||
|
modified_dirs.add(dir_name.split("/")[0])
|
||||||
|
|
||||||
|
# Parcourir les répertoires uniques et commiter
|
||||||
|
for directory in modified_dirs:
|
||||||
|
print(f"Processing directory: {directory}")
|
||||||
|
|
||||||
|
# Ajoute tous les fichiers du répertoire
|
||||||
|
if os.path.exists(os.path.join(config.path, directory)):
|
||||||
|
mig_prefix_msg = "MIG"
|
||||||
|
run_git_command(f'cd "{config.path}" && git add {directory}')
|
||||||
|
|
||||||
|
# Vérifie si le répertoire a des changements staged
|
||||||
|
commit_result_git_diff = run_git_command(
|
||||||
|
f'cd "{config.path}" && git diff --cached --quiet {directory}',
|
||||||
|
ignore_error=True,
|
||||||
|
)
|
||||||
|
if commit_result_git_diff.returncode == 0:
|
||||||
|
print(f"No changes to commit in {directory}.")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
mig_prefix_msg = "DEL"
|
||||||
|
run_git_command(f'cd "{config.path}" && git rm -r {directory}')
|
||||||
|
|
||||||
|
# Crée le commit
|
||||||
|
commit_message = (
|
||||||
|
f"[{mig_prefix_msg}] {directory}: Migration to {config.odoo_version}"
|
||||||
|
)
|
||||||
|
commit_result = run_git_command(
|
||||||
|
f'cd "{config.path}" && git commit -m "{commit_message}"'
|
||||||
|
)
|
||||||
|
|
||||||
|
if commit_result.returncode == 0:
|
||||||
|
print(f"Commit successful for {directory}.")
|
||||||
|
|
||||||
|
print("\nAll directories with changes have been processed.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
commit_by_directory()
|
||||||
98
script/code/odoo_upgrade_code_with_dir_module.py
Executable file
98
script/code/odoo_upgrade_code_with_dir_module.py
Executable file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import asyncio.subprocess as asp
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# TODO this script need odoo 18
|
||||||
|
|
||||||
|
|
||||||
|
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="""\
|
||||||
|
|
||||||
|
""",
|
||||||
|
epilog="""\
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--path", required=True, help="Path to directory to migrate all module"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
async def run_cmd(cmd: str):
|
||||||
|
"""Run a command asynchronously and log output."""
|
||||||
|
proc = await asp.create_subprocess_shell(
|
||||||
|
cmd,
|
||||||
|
stdout=asp.PIPE,
|
||||||
|
stderr=asp.PIPE,
|
||||||
|
)
|
||||||
|
stdout, stderr = await proc.communicate()
|
||||||
|
|
||||||
|
if stdout:
|
||||||
|
_logger.info(stdout.decode().strip())
|
||||||
|
if stderr:
|
||||||
|
_logger.error(stderr.decode().strip())
|
||||||
|
|
||||||
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate_modules(config):
|
||||||
|
"""Find Odoo modules and run migration script on each."""
|
||||||
|
chemin_du_dossier = Path(config.path)
|
||||||
|
if not chemin_du_dossier.exists():
|
||||||
|
die(True, f"Path {chemin_du_dossier} does not exist")
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for element in chemin_du_dossier.iterdir():
|
||||||
|
manifest = element / "__manifest__.py"
|
||||||
|
if element.is_dir() and manifest.exists():
|
||||||
|
cmd = f"./script/code/odoo_upgrade_code_with_single_module_autosearch.sh {element.name}"
|
||||||
|
_logger.info("Executing: %s", cmd)
|
||||||
|
tasks.append(run_cmd(cmd))
|
||||||
|
|
||||||
|
if tasks:
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
for idx, res in enumerate(results):
|
||||||
|
if isinstance(res, Exception):
|
||||||
|
_logger.error("Task %d failed with exception: %s", idx, res)
|
||||||
|
elif res != 0:
|
||||||
|
_logger.error("Task %d exited with code %d", idx, res)
|
||||||
|
else:
|
||||||
|
_logger.info("Task %d completed successfully", idx)
|
||||||
|
else:
|
||||||
|
_logger.warning("No modules found in %s", chemin_du_dossier)
|
||||||
|
|
||||||
|
|
||||||
|
def die(cond, message, code=1):
|
||||||
|
if cond:
|
||||||
|
print(message, file=sys.stderr)
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_main():
|
||||||
|
config = get_config()
|
||||||
|
await migrate_modules(config)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(async_main())
|
||||||
5
script/code/odoo_upgrade_code_with_single_module.sh
Executable file
5
script/code/odoo_upgrade_code_with_single_module.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# you need odoo 18 installed
|
||||||
|
# you need $1 path to the module
|
||||||
|
./odoo18.0/odoo/odoo-bin upgrade_code --from 12.0 --addons-path "$1"
|
||||||
14
script/code/odoo_upgrade_code_with_single_module_autosearch.sh
Executable file
14
script/code/odoo_upgrade_code_with_single_module_autosearch.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
Red='\033[0;31m' # Red
|
||||||
|
Color_Off='\033[0m' # Text Reset
|
||||||
|
|
||||||
|
# you need odoo 18 installed
|
||||||
|
# you need $1 for the module name
|
||||||
|
output=$(./script/addons/check_addons_exist.py --output_path -m "$1")
|
||||||
|
retVal=$?
|
||||||
|
if [[ $retVal -ne 0 ]]; then
|
||||||
|
echo -e "${Red}Error${Color_Off} check_addons_exist.py into odoo_upgrade_code_with_single_module_autosearch.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
./script/code/odoo_upgrade_code_with_single_module.sh "$output"
|
||||||
0
script/config/__init__.py
Normal file
0
script/config/__init__.py
Normal file
56
script/config/config_file.py
Normal file
56
script/config/config_file.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
CONFIG_FILE = "./script/todo/todo.json"
|
||||||
|
CONFIG_OVERRIDE_FILE = "./private/todo/todo_override.json"
|
||||||
|
CONFIG_OVERRIDE_PRIVATE_FILE = "./private/todo/todo_override_private.json"
|
||||||
|
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format=(
|
||||||
|
"%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d]"
|
||||||
|
" %(message)s"
|
||||||
|
),
|
||||||
|
datefmt="%Y-%m-%d:%H:%M:%S",
|
||||||
|
level=logging.INFO,
|
||||||
|
)
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFile:
|
||||||
|
def get_config(self, lst_params):
|
||||||
|
# Open file
|
||||||
|
config_file = CONFIG_FILE
|
||||||
|
if os.path.exists(CONFIG_OVERRIDE_FILE):
|
||||||
|
config_file = CONFIG_OVERRIDE_FILE
|
||||||
|
|
||||||
|
find_in_private = False
|
||||||
|
if os.path.exists(CONFIG_OVERRIDE_PRIVATE_FILE):
|
||||||
|
with open(CONFIG_OVERRIDE_PRIVATE_FILE) as cfg:
|
||||||
|
dct_data = json.load(cfg)
|
||||||
|
for param in lst_params:
|
||||||
|
if param in dct_data.keys():
|
||||||
|
find_in_private = True
|
||||||
|
dct_data = dct_data[param]
|
||||||
|
|
||||||
|
if not find_in_private:
|
||||||
|
with open(config_file) as cfg:
|
||||||
|
dct_data = json.load(cfg)
|
||||||
|
for param in lst_params:
|
||||||
|
try:
|
||||||
|
dct_data = dct_data[param]
|
||||||
|
except KeyError:
|
||||||
|
_logger.error(
|
||||||
|
f"KeyError on file {config_file} with keys"
|
||||||
|
f" {lst_params}"
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
return dct_data
|
||||||
|
|
||||||
|
def get_logo_ascii_file_path(self):
|
||||||
|
return LOGO_ASCII_FILE
|
||||||
|
|
@ -30,6 +30,10 @@ def get_config():
|
||||||
help="test and test_* and other by system test.",
|
help="test and test_* and other by system test.",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--database",
|
||||||
|
help="Specify database to delete, separate by coma",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
@ -39,6 +43,16 @@ def main():
|
||||||
|
|
||||||
out_db = execute_shell("./odoo_bin.sh db --list")
|
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 = []
|
||||||
|
if config.database:
|
||||||
|
lst_database_to_delete = [
|
||||||
|
a.strip() for a in config.database.split(",")
|
||||||
|
]
|
||||||
|
|
||||||
|
cmd_all = "parallel :::"
|
||||||
|
cmd_end = ""
|
||||||
|
lst_db_name = []
|
||||||
for db_name in lst_db:
|
for db_name in lst_db:
|
||||||
if config.test_only and not (
|
if config.test_only and not (
|
||||||
db_name in ("test",)
|
db_name in ("test",)
|
||||||
|
|
@ -46,11 +60,16 @@ def main():
|
||||||
or db_name.startswith("new_project_")
|
or db_name.startswith("new_project_")
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
execute_shell(
|
if lst_database_to_delete and db_name not in lst_database_to_delete:
|
||||||
"./odoo_bin.sh db --drop --database"
|
continue
|
||||||
f" {db_name}"
|
|
||||||
)
|
cmd_end += f' "./odoo_bin.sh db --drop --database {db_name}"'
|
||||||
print(f"{db_name} deleted")
|
lst_db_name.append(db_name)
|
||||||
|
if cmd_end:
|
||||||
|
execute_shell(cmd_all + cmd_end)
|
||||||
|
print("Database deleted :")
|
||||||
|
for db_name in lst_db_name:
|
||||||
|
print(db_name)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,16 @@ SUGGESTION
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Delete all database cache to clone, begin by _cache_.",
|
help="Delete all database cache to clone, begin by _cache_.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ignore_cache",
|
||||||
|
action="store_true",
|
||||||
|
help="Ignore creating _cache_ when restoring.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--only_drop",
|
||||||
|
action="store_true",
|
||||||
|
help="Will only drop database if exist.",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
@ -122,8 +132,10 @@ def main():
|
||||||
arg = f"{arg_base} --drop --database {config.database}"
|
arg = f"{arg_base} --drop --database {config.database}"
|
||||||
out = check_output(arg.split(" ")).decode()
|
out = check_output(arg.split(" ")).decode()
|
||||||
print(out)
|
print(out)
|
||||||
|
if config.only_drop:
|
||||||
|
return
|
||||||
# Check cache exist
|
# Check cache exist
|
||||||
if cache_database not in lst_db_cache:
|
if cache_database not in lst_db_cache and not config.ignore_cache:
|
||||||
_logger.info(
|
_logger.info(
|
||||||
f"## Create cache {cache_database} from image"
|
f"## Create cache {cache_database} from image"
|
||||||
f" {config.image} ##"
|
f" {config.image} ##"
|
||||||
|
|
@ -135,13 +147,23 @@ def main():
|
||||||
out = check_output(arg.split(" ")).decode()
|
out = check_output(arg.split(" ")).decode()
|
||||||
print(out)
|
print(out)
|
||||||
# Clone database
|
# Clone database
|
||||||
_logger.info(
|
if config.ignore_cache:
|
||||||
f"## Clone cache {cache_database} to database {config.database} ##"
|
_logger.info(
|
||||||
)
|
f"## Restoring {config.image} to database {config.database} ##"
|
||||||
arg = (
|
)
|
||||||
f"{arg_base} --clone --from_database"
|
arg = (
|
||||||
f" {cache_database} --database {config.database}"
|
f"{arg_base} --restore --restore_image"
|
||||||
)
|
f" {config.image} --database {config.database}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_logger.info(
|
||||||
|
f"## Clone cache {cache_database} to database {config.database} ##"
|
||||||
|
)
|
||||||
|
arg = (
|
||||||
|
f"{arg_base} --clone --from_database"
|
||||||
|
f" {cache_database} --database {config.database}"
|
||||||
|
)
|
||||||
|
print(arg)
|
||||||
out = check_output(arg.split(" ")).decode()
|
out = check_output(arg.split(" ")).decode()
|
||||||
print(out)
|
print(out)
|
||||||
|
|
||||||
|
|
|
||||||
90
script/database/download_remote.py
Normal file
90
script/database/download_remote.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2024 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
print("This script only work with localhost:8069, not working with remote instance.")
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ODOO_URL = '' # Your Odoo server URL
|
||||||
|
DATABASE_NAME = ''
|
||||||
|
MASTER_PASSWORD = ''
|
||||||
|
# BACKUP_FORMAT = env('BACKUP_FORMAT', default='zip') # 'zip' or 'dump'
|
||||||
|
BACKUP_FORMAT = 'zip' # 'zip' or 'dump'
|
||||||
|
OUTPUT_FILE_NAME = f'{DATABASE_NAME}_backup.{BACKUP_FORMAT}'
|
||||||
|
|
||||||
|
# --- Logger Setup ---
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# TODO not working with remote, only localhost
|
||||||
|
logger.error("This script is not working with remote instance, only http://127.0.0.1:8069")
|
||||||
|
|
||||||
|
# --- Function to download the database ---
|
||||||
|
def download_odoo_db():
|
||||||
|
logger.info(f"Attempting to download database '{DATABASE_NAME}' from URL '{ODOO_URL}'...")
|
||||||
|
|
||||||
|
# URL for the backup endpoint
|
||||||
|
backup_url = f'{ODOO_URL}/web/database/backup'
|
||||||
|
|
||||||
|
# Form data for the POST request
|
||||||
|
payload = {
|
||||||
|
'master_pwd': MASTER_PASSWORD,
|
||||||
|
'name': DATABASE_NAME,
|
||||||
|
'backup_format': BACKUP_FORMAT,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute the POST request to start the backup
|
||||||
|
response = requests.post(backup_url, data=payload, stream=True)
|
||||||
|
response.raise_for_status() # Raise an exception for bad status codes
|
||||||
|
|
||||||
|
# --- VALIDATION ---
|
||||||
|
content_type = response.headers.get('Content-Type', '').split(';')[0]
|
||||||
|
|
||||||
|
# Check if the content type is valid
|
||||||
|
expected_types = ['application/zip', 'application/octet-stream']
|
||||||
|
if content_type not in expected_types:
|
||||||
|
logger.error(f"ERROR: Expected one of {expected_types} but got {content_type}.")
|
||||||
|
logger.error("This usually indicates a server-side error or an incorrect master password.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Check if the content is an HTML page (to handle incorrect passwords)
|
||||||
|
first_chunk = next(response.iter_content(chunk_size=128), b'')
|
||||||
|
if first_chunk.startswith(b'<'):
|
||||||
|
logger.error("ERROR: It seems the server returned an HTML page instead of a database file.")
|
||||||
|
logger.error("This is often due to an incorrect master password or an invalid request.")
|
||||||
|
logger.error("Server Response (First 200 chars):")
|
||||||
|
logger.error(response.text[:200])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- DOWNLOAD ---
|
||||||
|
logger.info(f"Download started, saving to '{OUTPUT_FILE_NAME}'...")
|
||||||
|
|
||||||
|
output_path = Path(OUTPUT_FILE_NAME)
|
||||||
|
with open(output_path, 'wb') as f:
|
||||||
|
# Write the content from the first chunk to the file
|
||||||
|
f.write(first_chunk)
|
||||||
|
# Continue writing the rest of the stream in chunks
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
logger.info(f"Download successful! File saved at: {output_path.resolve()}")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"An error occurred while connecting to the Odoo server: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Script execution ---
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Add a simple check to ensure the user has configured the variables
|
||||||
|
if ODOO_URL == 'http://localhost:8069' and DATABASE_NAME == 'your_database_name':
|
||||||
|
logger.error("Please configure the ODOO_URL, DATABASE_NAME, and MASTER_PASSWORD variables at the start of the script.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
download_odoo_db()
|
||||||
92
script/database/download_remote.sh
Executable file
92
script/database/download_remote.sh
Executable file
|
|
@ -0,0 +1,92 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
# Set variable
|
||||||
|
# export MASTER_PWD=""
|
||||||
|
# export DATABASE_NAME=""
|
||||||
|
# export OUTPUT_FILE_PATH=""
|
||||||
|
# export ODOO_URL=""
|
||||||
|
|
||||||
|
#MASTER_PWD="ADMIN"
|
||||||
|
#DATABASE_NAME="BD"
|
||||||
|
BACKUP_FORMAT="zip"
|
||||||
|
#OUTPUT_FILE_PATH="/tmp/test.zip"
|
||||||
|
#ODOO_URL="https://mondomain"
|
||||||
|
|
||||||
|
QUIET_MODE=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--quiet)
|
||||||
|
QUIET_MODE=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Argument non reconnu : $arg"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Security Check ---
|
||||||
|
# Check if the MASTER_PWD environment variable is set
|
||||||
|
if [[ -z "$MASTER_PWD" ]]; then
|
||||||
|
echo "Error: The MASTER_PWD environment variable is not set." >&2
|
||||||
|
echo "Please set it before running this script:" >&2
|
||||||
|
echo " export MASTER_PWD='your_master_password'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Check if the ODOO_URL environment variable is set
|
||||||
|
if [[ -z "$ODOO_URL" ]]; then
|
||||||
|
# echo "Error: The ODOO_URL environment variable is not set." >&2
|
||||||
|
# echo "Please set it before running this script:" >&2
|
||||||
|
# echo " export ODOO_URL='your_master_password'" >&2
|
||||||
|
# exit 1
|
||||||
|
read -p "Odoo URL: " ODOO_URL
|
||||||
|
fi
|
||||||
|
# Check if the DATABASE_NAME environment variable is set
|
||||||
|
if [[ -z "$DATABASE_NAME" ]]; then
|
||||||
|
# echo "Error: The DATABASE_NAME environment variable is not set." >&2
|
||||||
|
# echo "Please set it before running this script:" >&2
|
||||||
|
# echo " export DATABASE_NAME='your_master_password'" >&2
|
||||||
|
# exit 1
|
||||||
|
read -p "Database: " DATABASE_NAME
|
||||||
|
fi
|
||||||
|
# Check if the OUTPUT_FILE_PATH environment variable is set
|
||||||
|
if [[ -z "$OUTPUT_FILE_PATH" ]]; then
|
||||||
|
# echo "Error: The OUTPUT_FILE_PATH environment variable is not set." >&2
|
||||||
|
# echo "Please set it before running this script:" >&2
|
||||||
|
# echo " export OUTPUT_FILE_PATH='your_master_password'" >&2
|
||||||
|
# exit 1
|
||||||
|
read -p "Output File Path: " OUTPUT_FILE_PATH
|
||||||
|
fi
|
||||||
|
|
||||||
|
ODOO_BACKUP_URL="${ODOO_URL}/web/database/backup"
|
||||||
|
|
||||||
|
# --- Curl Command to Download Database ---
|
||||||
|
echo "Starting Odoo database backup for '${DATABASE_NAME}' from '${ODOO_BACKUP_URL}' to path '${OUTPUT_FILE_PATH}'..."
|
||||||
|
|
||||||
|
if [ "$QUIET_MODE" = false ]; then
|
||||||
|
curl -X POST \
|
||||||
|
-F "master_pwd=$MASTER_PWD" \
|
||||||
|
-F "name=$DATABASE_NAME" \
|
||||||
|
-F "backup_format=$BACKUP_FORMAT" \
|
||||||
|
-o "$OUTPUT_FILE_PATH" \
|
||||||
|
--progress-bar \
|
||||||
|
"$ODOO_BACKUP_URL"
|
||||||
|
else
|
||||||
|
curl -X POST \
|
||||||
|
-F "master_pwd=$MASTER_PWD" \
|
||||||
|
-F "name=$DATABASE_NAME" \
|
||||||
|
-F "backup_format=$BACKUP_FORMAT" \
|
||||||
|
-o "$OUTPUT_FILE_PATH" \
|
||||||
|
"$ODOO_BACKUP_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Verification ---
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
echo "Backup completed successfully!"
|
||||||
|
echo "File saved to: $OUTPUT_FILE_PATH"
|
||||||
|
else
|
||||||
|
echo "Backup failed. Please check the logs." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -100,7 +100,7 @@ def main():
|
||||||
if not os.path.isfile(filename_odoo_version):
|
if not os.path.isfile(filename_odoo_version):
|
||||||
_logger.error(f"Missing file {filename_odoo_version}")
|
_logger.error(f"Missing file {filename_odoo_version}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
with open(".odoo-version", "r") as f:
|
with open(filename_odoo_version, "r") as f:
|
||||||
config.odoo_version = f.readline()
|
config.odoo_version = f.readline()
|
||||||
|
|
||||||
# Open configuration file
|
# Open configuration file
|
||||||
|
|
|
||||||
60
script/database/list_remote.py
Executable file
60
script/database/list_remote.py
Executable file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
import xmlrpc.client
|
||||||
|
import sys
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
def get_db_list_xmlrpc(odoo_url):
|
||||||
|
"""
|
||||||
|
Retrieves the list of Odoo databases using the XML-RPC API.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
common = xmlrpc.client.ServerProxy(f'{odoo_url}/xmlrpc/db')
|
||||||
|
db_list = common.list()
|
||||||
|
return db_list
|
||||||
|
except xmlrpc.client.Fault as e:
|
||||||
|
print(f"XML-RPC Error: {e.faultCode} - {e.faultString}", file=sys.stderr)
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Connection Error: {e}", file=sys.stderr)
|
||||||
|
return []
|
||||||
|
|
||||||
|
# --- CLI using Click ---
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
'--odoo-url',
|
||||||
|
default='http://localhost:8069',
|
||||||
|
help='URL of the Odoo server.',
|
||||||
|
show_default=True
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--raw',
|
||||||
|
is_flag=True,
|
||||||
|
help='Output one database per line, without extra formatting. Useful for scripting.'
|
||||||
|
)
|
||||||
|
def list_databases(odoo_url, raw=False):
|
||||||
|
"""
|
||||||
|
This script lists all available databases on an Odoo server.
|
||||||
|
"""
|
||||||
|
if not raw:
|
||||||
|
click.echo(f"Attempting to connect to Odoo at: {odoo_url}")
|
||||||
|
|
||||||
|
databases = get_db_list_xmlrpc(odoo_url)
|
||||||
|
|
||||||
|
if databases:
|
||||||
|
if not raw:
|
||||||
|
click.echo("\nAvailable databases:")
|
||||||
|
for db in databases:
|
||||||
|
if not raw:
|
||||||
|
click.echo(f"- {db}")
|
||||||
|
else:
|
||||||
|
click.echo(db)
|
||||||
|
else:
|
||||||
|
if not raw:
|
||||||
|
click.echo("Failed to retrieve the database list.")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Script Execution ---
|
||||||
|
if __name__ == '__main__':
|
||||||
|
list_databases()
|
||||||
9
script/odoo/migration/README.md
Normal file
9
script/odoo/migration/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Migration
|
||||||
|
|
||||||
|
Run this script when doing database migration. Example :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat ./script/migration/fix_migration_odoo140_to_odoo150.py | ./odoo15.0/odoo/odoo-bin shell -d DATABASE
|
||||||
|
```
|
||||||
|
|
||||||
|
Check [uninstall_module_list_odoo140_to_odoo150.txt](uninstall_module_list_odoo140_to_odoo150.txt)
|
||||||
24
script/odoo/migration/fix_migration_odoo140_to_odoo150.py
Normal file
24
script/odoo/migration/fix_migration_odoo140_to_odoo150.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
# This script need to be run when upgrade 14 to 15 when database is created from ERPLibre.
|
||||||
|
print("Running a script in the Odoo shell!")
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
print(
|
||||||
|
f"{i}. Remove project_type because OCA project_category migrating to OCA project_type and conflict with odoo14.0/addons/Numigi_odoo-project-addons"
|
||||||
|
)
|
||||||
|
# TODO do a migration, copy data to another temporary table
|
||||||
|
env["ir.module.module"].search([("name", "=", "project_type")]).unlink()
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# print(
|
||||||
|
# f"{i}. Remove project_type because OCA project_category migrating to OCA project_type"
|
||||||
|
# )
|
||||||
|
# env["ir.module.module"].search([("name", "=", "l10n_eu_oss_oca")]).unlink()
|
||||||
|
# i += 1
|
||||||
|
|
||||||
|
env.cr.commit()
|
||||||
|
|
||||||
|
print("End fix migration Odoo 14.0 to Odoo 15.0")
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
muk_web_theme
|
||||||
|
|
@ -2,13 +2,27 @@
|
||||||
# Open a new gnome-terminal with different path on new tab
|
# Open a new gnome-terminal with different path on new tab
|
||||||
ODOO_VERSION=$(cat .odoo-version)
|
ODOO_VERSION=$(cat .odoo-version)
|
||||||
working_path=$(readlink -f .)
|
working_path=$(readlink -f .)
|
||||||
|
|
||||||
|
# TODO fix open_terminal.sh
|
||||||
|
#paths="${working_path}/
|
||||||
|
#${working_path}/
|
||||||
|
#${working_path}/odoo${ODOO_VERSION}/addons/ERPLibre_erplibre_addons
|
||||||
|
#${working_path}/odoo${ODOO_VERSION}/addons/TechnoLibre_odoo-code-generator
|
||||||
|
#${working_path}/odoo${ODOO_VERSION}/addons/TechnoLibre_odoo-code-generator-template"
|
||||||
|
#
|
||||||
|
## "${working_path}/odoo${ODOO_VERSION}/addons/OCA_server-tools"
|
||||||
|
#
|
||||||
|
#cmd="git status"
|
||||||
|
##echo "${paths}"
|
||||||
|
#./script/terminal/open_terminal.sh "$cmd" "$paths"
|
||||||
|
|
||||||
paths=(
|
paths=(
|
||||||
"${working_path}/"
|
"${working_path}/"
|
||||||
"${working_path}/"
|
"${working_path}/"
|
||||||
"${working_path}/addons.odoo${ODOO_VERSION}/ERPLibre_erplibre_addons"
|
"${working_path}/odoo${ODOO_VERSION}/addons/ERPLibre_erplibre_addons"
|
||||||
"${working_path}/addons.odoo${ODOO_VERSION}/TechnoLibre_odoo-code-generator"
|
"${working_path}/odoo${ODOO_VERSION}/addons/TechnoLibre_odoo-code-generator"
|
||||||
"${working_path}/addons.odoo${ODOO_VERSION}/TechnoLibre_odoo-code-generator-template"
|
"${working_path}/odoo${ODOO_VERSION}/addons/TechnoLibre_odoo-code-generator-template"
|
||||||
# "${working_path}/addons.odoo${ODOO_VERSION}/OCA_server-tools"
|
# "${working_path}/odoo${ODOO_VERSION}/addons/OCA_server-tools"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16,7 +30,7 @@ first_iteration=true
|
||||||
second_iteration=true
|
second_iteration=true
|
||||||
if [[ "${OSTYPE}" == "linux-gnu" ]]; then
|
if [[ "${OSTYPE}" == "linux-gnu" ]]; then
|
||||||
cmd_before="cd "
|
cmd_before="cd "
|
||||||
cmd_after_first=";gnome-terminal --tab -- bash -c 'source ./.venv/bin/activate;git status;bash';"
|
cmd_after_first=";gnome-terminal --tab -- bash -c 'source ./.venv.erplibre/bin/activate;git status;bash';"
|
||||||
cmd_after=";gnome-terminal --tab -- bash -c 'git status;bash';"
|
cmd_after=";gnome-terminal --tab -- bash -c 'git status;bash';"
|
||||||
LONGCMD=""
|
LONGCMD=""
|
||||||
for t in "${paths[@]}"; do
|
for t in "${paths[@]}"; do
|
||||||
|
|
@ -43,17 +57,17 @@ elif [[ "${OSTYPE}" == "darwin"* ]]; then
|
||||||
# Boucle pour ajouter des commandes pour ouvrir de nouveaux onglets et exécuter les scripts batch
|
# Boucle pour ajouter des commandes pour ouvrir de nouveaux onglets et exécuter les scripts batch
|
||||||
for t in "${paths[@]}"; do
|
for t in "${paths[@]}"; do
|
||||||
if $first_iteration; then
|
if $first_iteration; then
|
||||||
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${t}; source ./.venv/bin/activate; git status\" in front window'"
|
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${t}; source ./.venv.erplibre/bin/activate; git status\" in front window'"
|
||||||
first_iteration=false
|
first_iteration=false
|
||||||
elif $second_iteration; then
|
elif $second_iteration; then
|
||||||
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${t}; source ./.venv/bin/activate; git status\" in front window'"
|
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${t}; source ./.venv.erplibre/bin/activate; git status\" in front window'"
|
||||||
second_iteration=false
|
second_iteration=false
|
||||||
else
|
else
|
||||||
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${t}; git status\" in front window'"
|
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${t}; git status\" in front window'"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
osascript_command+=" -e 'end tell'"
|
osascript_command+=" -e 'end tell'"
|
||||||
|
echo "$osascript_command"
|
||||||
# Exécution de la commande osascript
|
# Exécution de la commande osascript
|
||||||
eval "$osascript_command"
|
eval "$osascript_command"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
98
script/terminal/open_terminal.sh
Executable file
98
script/terminal/open_terminal.sh
Executable file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
CMD_TO_EXEC="$1"
|
||||||
|
PATHS_RAW="$2"
|
||||||
|
#paths="$2"
|
||||||
|
echo "Script start open_terminal.sh"
|
||||||
|
echo "$PATHS_RAW"
|
||||||
|
IFS=$'\n' read -r -d '' -a paths <<< "$PATHS_RAW"
|
||||||
|
echo "$path"
|
||||||
|
SOURCE_CMD="source ./.venv.erplibre/bin/activate"
|
||||||
|
FIRST_ITERATION=true
|
||||||
|
SECOND_ITERATION=true
|
||||||
|
|
||||||
|
which gnome-terminal
|
||||||
|
GNOME_TERMINAL_CMD=$(which gnome-terminal)
|
||||||
|
|
||||||
|
HAS_GNOME_TERMINAL=false
|
||||||
|
HAS_TELL_TERMINAL=false
|
||||||
|
if command -v gnome-terminal &>/dev/null; then
|
||||||
|
HAS_GNOME_TERMINAL=true
|
||||||
|
echo "Detect gnome-terminal"
|
||||||
|
|
||||||
|
##elif command -v tell &>/dev/null; then
|
||||||
|
#elif [[ "${OSTYPE}" == "darwin"* ]] && command -v osascript &>/dev/null; then
|
||||||
|
#if [[ "${OSTYPE}" == "darwin"* ]] && command -v osascript &>/dev/null; then
|
||||||
|
# TODO validate osascript is implemented
|
||||||
|
elif [[ "${OSTYPE}" == "darwin"* ]] then
|
||||||
|
echo "Detect osascript Darwin"
|
||||||
|
HAS_TELL_TERMINAL=true
|
||||||
|
else
|
||||||
|
echo "Detect cli"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$paths" ]]; then
|
||||||
|
working_path=$(readlink -f .)
|
||||||
|
paths="${working_path}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#if [[ "${OSTYPE}" == "linux-gnu" ]]; then
|
||||||
|
if [ "$HAS_GNOME_TERMINAL" = true ]; then
|
||||||
|
CMD_BEFORE="cd "
|
||||||
|
CMD_AFTER_FIRST=";gnome-terminal --tab -- /bin/bash -c '${SOURCE_CMD};${CMD_TO_EXEC};bash';"
|
||||||
|
CMD_AFTER=";gnome-terminal --tab -- /bin/bash -c '${CMD_TO_EXEC};bash';"
|
||||||
|
LONGCMD=""
|
||||||
|
for PATH in "${paths[@]}"; do
|
||||||
|
if [[ ! -e "${PATH}" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if $FIRST_ITERATION; then
|
||||||
|
LONGCMD+="${CMD_BEFORE}${PATH}${CMD_AFTER_FIRST}"
|
||||||
|
FIRST_ITERATION=false
|
||||||
|
else
|
||||||
|
LONGCMD+="${CMD_BEFORE}${PATH}${CMD_AFTER}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "${LONGCMD}"
|
||||||
|
echo "${GNOME_TERMINAL_CMD}"
|
||||||
|
# $GNOME_TERMINAL_CMD --window -- /bin/bash -c "ls"
|
||||||
|
$GNOME_TERMINAL_CMD --window -- /bin/bash -c "${LONGCMD}"
|
||||||
|
elif [ "$HAS_TELL_TERMINAL" = true ]; then
|
||||||
|
paths=("${paths[@]:1}")
|
||||||
|
|
||||||
|
# Initialisation de la commande osascript
|
||||||
|
osascript_command="osascript -e 'tell application \"Terminal\"'"
|
||||||
|
|
||||||
|
# Boucle pour ajouter des commandes pour ouvrir de nouveaux onglets et exécuter les scripts batch
|
||||||
|
for PATH in "${paths[@]}"; do
|
||||||
|
if [[ ! -e "${PATH}" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if $FIRST_ITERATION; then
|
||||||
|
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${PATH}; ${SOURCE_CMD}; ${CMD_TO_EXEC}\" in front window'"
|
||||||
|
FIRST_ITERATION=false
|
||||||
|
elif $SECOND_ITERATION; then
|
||||||
|
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${PATH}; ${SOURCE_CMD}; ${CMD_TO_EXEC}\" in front window'"
|
||||||
|
SECOND_ITERATION=false
|
||||||
|
else
|
||||||
|
osascript_command+=" -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \"cd ${PATH}; ${SOURCE_CMD}; ${CMD_TO_EXEC}\" in front window'"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
osascript_command+=" -e 'end tell'"
|
||||||
|
|
||||||
|
# Exécution de la commande osascript
|
||||||
|
echo "${osascript_command}"
|
||||||
|
eval "$osascript_command"
|
||||||
|
else
|
||||||
|
# echo "CLI"
|
||||||
|
# for PATH in "${paths[@]}"; do
|
||||||
|
# if [[ ! -e "${PATH}" ]]; then
|
||||||
|
# continue
|
||||||
|
# fi
|
||||||
|
# cd ${PATH}
|
||||||
|
# echo "${CMD_TO_EXEC}"
|
||||||
|
# eval ${CMD_TO_EXEC}
|
||||||
|
# done
|
||||||
|
echo "Cannot find gnome-terminal (GNOME) or osasscript (OSX)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
27
script/terminal/validate_to_continue.sh
Executable file
27
script/terminal/validate_to_continue.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "Usage: $0 \"Warning message to display\"" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg="$1"
|
||||||
|
|
||||||
|
# Require an interactive terminal
|
||||||
|
if [[ ! -t 0 || ! -t 1 ]]; then
|
||||||
|
echo "Interactive confirmation not possible (no TTY). Aborting." >&2
|
||||||
|
exit 130
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$msg"
|
||||||
|
printf "Proceed? [y/N] "
|
||||||
|
# shellcheck disable=SC2162
|
||||||
|
read -r ans
|
||||||
|
|
||||||
|
if [[ "$ans" =~ ^[yY]$ ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Aborted."
|
||||||
|
exit 130
|
||||||
|
fi
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
TODO is an assistant robot to use ERPLibre
|
TODO is an assistant robot to use ERPLibre
|
||||||
Execute it with `./script/todo/todo.py` or `make todo`.
|
Execute it with `./script/todo/todo.py` or `make todo`.
|
||||||
|
|
||||||
For a new project, copy todo_example.json to private/todo.json and edit it.
|
For a new project, copy todo_example.json to private/todo/todo_override.json | private/todo/todo_override_private.json and edit it.
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,14 @@
|
||||||
{
|
{
|
||||||
"prompt_description": "Show code status",
|
"prompt_description": "Show code status",
|
||||||
"makefile_cmd": "repo_show_status"
|
"makefile_cmd": "repo_show_status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_description": "Stash all code",
|
||||||
|
"makefile_cmd": "repo_do_stash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt_description": "Format modified code",
|
||||||
|
"makefile_cmd": "format"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import getpass
|
import getpass
|
||||||
|
|
@ -9,6 +11,14 @@ import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
new_path = os.path.normpath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
||||||
|
)
|
||||||
|
sys.path.append(new_path)
|
||||||
|
|
||||||
|
from script.config import config_file
|
||||||
|
|
||||||
file_error_path = ".erplibre.error.txt"
|
file_error_path = ".erplibre.error.txt"
|
||||||
cst_venv_erplibre = ".venv.erplibre"
|
cst_venv_erplibre = ".venv.erplibre"
|
||||||
|
|
@ -16,7 +26,7 @@ VERSION_DATA_FILE = os.path.join("conf", "supported_version_erplibre.json")
|
||||||
INSTALLED_ODOO_VERSION_FILE = os.path.join(
|
INSTALLED_ODOO_VERSION_FILE = os.path.join(
|
||||||
".repo", "installed_odoo_version.txt"
|
".repo", "installed_odoo_version.txt"
|
||||||
)
|
)
|
||||||
ODOO_VERSION_FILE = os.path.join(".odoo-version")
|
ODOO_VERSION_FILE = ".odoo-version"
|
||||||
ENABLE_CRASH = False
|
ENABLE_CRASH = False
|
||||||
CRASH_E = None
|
CRASH_E = None
|
||||||
|
|
||||||
|
|
@ -27,12 +37,13 @@ try:
|
||||||
import click
|
import click
|
||||||
import humanize
|
import humanize
|
||||||
import openai
|
import openai
|
||||||
from pykeepass import PyKeePass
|
import todo_file_browser
|
||||||
|
|
||||||
# TODO implement urwid to improve text user interface
|
|
||||||
# import urwid
|
# import urwid
|
||||||
# TODO implement rich for beautiful print and table
|
# TODO implement rich for beautiful print and table
|
||||||
# import rich
|
# import rich
|
||||||
|
import todo_upgrade
|
||||||
|
from pykeepass import PyKeePass
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
humanize = None
|
humanize = None
|
||||||
ENABLE_CRASH = True
|
ENABLE_CRASH = True
|
||||||
|
|
@ -41,35 +52,48 @@ except ModuleNotFoundError as e:
|
||||||
if not ENABLE_CRASH:
|
if not ENABLE_CRASH:
|
||||||
print("Importation success!")
|
print("Importation success!")
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format=(
|
||||||
|
"%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d]"
|
||||||
|
" %(message)s"
|
||||||
|
),
|
||||||
|
datefmt="%Y-%m-%d:%H:%M:%S",
|
||||||
|
level=logging.INFO,
|
||||||
|
)
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_FILE = "./script/todo/todo.json"
|
CONFIG_FILE = "./script/todo/todo.json"
|
||||||
CONFIG_OVERRIDE_FILE = "./private/todo.json"
|
CONFIG_OVERRIDE_FILE = "./private/todo/todo.json"
|
||||||
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
|
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
|
||||||
|
|
||||||
|
|
||||||
class TODO:
|
class TODO:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.dir_path = None
|
||||||
self.kdbx = None
|
self.kdbx = None
|
||||||
self.init()
|
self.init()
|
||||||
|
self.file_path = None
|
||||||
|
self.config_file = config_file.ConfigFile()
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
# Get command
|
# Get command
|
||||||
self.cmd_source_erplibre = ""
|
self.cmd_source_erplibre = ""
|
||||||
|
self.cmd_source_default = ""
|
||||||
exec_path_gnome_terminal = shutil.which("gnome-terminal")
|
exec_path_gnome_terminal = shutil.which("gnome-terminal")
|
||||||
if exec_path_gnome_terminal:
|
if exec_path_gnome_terminal:
|
||||||
self.cmd_source_erplibre = (
|
self.cmd_source_erplibre = (
|
||||||
f"gnome-terminal -- bash -c 'source"
|
f"gnome-terminal -- bash -c 'source"
|
||||||
f" ./{cst_venv_erplibre}/bin/activate;%s'"
|
f" ./{cst_venv_erplibre}/bin/activate;%s'"
|
||||||
)
|
)
|
||||||
|
self.cmd_source_default = "gnome-terminal -- bash -c '" f"%s'"
|
||||||
else:
|
else:
|
||||||
exec_path_tell = shutil.which("osascript")
|
exec_path_tell = shutil.which("osascript")
|
||||||
if exec_path_tell:
|
if exec_path_tell:
|
||||||
self.cmd_source_erplibre = (
|
self.cmd_source_erplibre = (
|
||||||
"osascript -e 'tell application \"Terminal\"'"
|
"osascript -e 'tell application \"Terminal\"'"
|
||||||
)
|
)
|
||||||
self.cmd_source_erplibre += " -e 'tell application \"System Events\" to keystroke \"PATH\" using {command down}' -e 'delay 0.1' -e 'do script \""
|
self.cmd_source_erplibre += " -e 'tell application \"System Events\" to keystroke \"t\" using {command down}' -e 'delay 0.1' -e 'do script \""
|
||||||
self.cmd_source_erplibre += f"./{cst_venv_erplibre}/bin/activate; %s\" in front window'"
|
self.cmd_source_erplibre += f"cd {os.getcwd()}; source ./{cst_venv_erplibre}/bin/activate; %s\" in front window'"
|
||||||
self.cmd_source_erplibre += " -e 'end tell'"
|
self.cmd_source_erplibre += " -e 'end tell'"
|
||||||
else:
|
else:
|
||||||
self.cmd_source_erplibre = (
|
self.cmd_source_erplibre = (
|
||||||
|
|
@ -77,7 +101,7 @@ class TODO:
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
with open(LOGO_ASCII_FILE) as my_file:
|
with open(self.config_file.get_logo_ascii_file_path()) as my_file:
|
||||||
print(my_file.read())
|
print(my_file.read())
|
||||||
print("Ouverture de TODO en cours ...")
|
print("Ouverture de TODO en cours ...")
|
||||||
print("🤖 => Entre tes directives par son chiffre et fait Entrée!")
|
print("🤖 => Entre tes directives par son chiffre et fait Entrée!")
|
||||||
|
|
@ -93,12 +117,14 @@ class TODO:
|
||||||
status = click.prompt(help_info)
|
status = click.prompt(help_info)
|
||||||
except NameError:
|
except NameError:
|
||||||
print("Do")
|
print("Do")
|
||||||
print("source .venv.erplibre/bin/activate && make")
|
print(f"source ./{cst_venv_erplibre}/bin/activate && make")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Do")
|
print("Do")
|
||||||
print("source .venv.erplibre/bin/activate && make")
|
print(f"source ./{cst_venv_erplibre}/bin/activate && make")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
except click.exceptions.Abort:
|
||||||
|
sys.exit(0)
|
||||||
print()
|
print()
|
||||||
if status == "0":
|
if status == "0":
|
||||||
break
|
break
|
||||||
|
|
@ -127,7 +153,7 @@ class TODO:
|
||||||
if self.kdbx:
|
if self.kdbx:
|
||||||
return self.kdbx
|
return self.kdbx
|
||||||
# Open file
|
# Open file
|
||||||
chemin_fichier_kdbx = self.get_config(["kdbx", "path"])
|
chemin_fichier_kdbx = self.config_file.get_config(["kdbx", "path"])
|
||||||
if not chemin_fichier_kdbx:
|
if not chemin_fichier_kdbx:
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
root.withdraw() # Hide the main window
|
root.withdraw() # Hide the main window
|
||||||
|
|
@ -136,10 +162,12 @@ class TODO:
|
||||||
filetypes=(("KeepassX files", "*.kdbx"),),
|
filetypes=(("KeepassX files", "*.kdbx"),),
|
||||||
)
|
)
|
||||||
if not chemin_fichier_kdbx:
|
if not chemin_fichier_kdbx:
|
||||||
_logger.error(f"KDBX is not configured, please fill {CONFIG_FILE}")
|
_logger.error(
|
||||||
|
f"KDBX is not configured, please fill {self.config_file.CONFIG_FILE}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
mot_de_passe_kdbx = self.get_config(["kdbx", "password"])
|
mot_de_passe_kdbx = self.config_file.get_config(["kdbx", "password"])
|
||||||
if not mot_de_passe_kdbx:
|
if not mot_de_passe_kdbx:
|
||||||
mot_de_passe_kdbx = getpass.getpass(
|
mot_de_passe_kdbx = getpass.getpass(
|
||||||
prompt="Entrez votre mot de passe : "
|
prompt="Entrez votre mot de passe : "
|
||||||
|
|
@ -151,25 +179,6 @@ class TODO:
|
||||||
self.kdbx = kp
|
self.kdbx = kp
|
||||||
return kp
|
return kp
|
||||||
|
|
||||||
def get_config(self, lst_params):
|
|
||||||
# Open file
|
|
||||||
config_file = CONFIG_FILE
|
|
||||||
if os.path.exists(CONFIG_OVERRIDE_FILE):
|
|
||||||
config_file = CONFIG_OVERRIDE_FILE
|
|
||||||
|
|
||||||
with open(config_file) as cfg:
|
|
||||||
dct_data = json.load(cfg)
|
|
||||||
for param in lst_params:
|
|
||||||
try:
|
|
||||||
dct_data = dct_data[param]
|
|
||||||
except KeyError:
|
|
||||||
_logger.error(
|
|
||||||
f"KeyError on file {config_file} with keys"
|
|
||||||
f" {lst_params}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
return dct_data
|
|
||||||
|
|
||||||
def execute_prompt_ia(self):
|
def execute_prompt_ia(self):
|
||||||
while True:
|
while True:
|
||||||
help_info = """Commande :
|
help_info = """Commande :
|
||||||
|
|
@ -182,7 +191,7 @@ class TODO:
|
||||||
kp = self.get_kdbx()
|
kp = self.get_kdbx()
|
||||||
if not kp:
|
if not kp:
|
||||||
return
|
return
|
||||||
nom_configuration = self.get_config(
|
nom_configuration = self.config_file.get_config(
|
||||||
["kdbx_config", "openai", "kdbx_key"]
|
["kdbx_config", "openai", "kdbx_key"]
|
||||||
)
|
)
|
||||||
entry = kp.find_entries_by_title(nom_configuration, first=True)
|
entry = kp.find_entries_by_title(nom_configuration, first=True)
|
||||||
|
|
@ -199,10 +208,12 @@ class TODO:
|
||||||
|
|
||||||
def prompt_execute(self):
|
def prompt_execute(self):
|
||||||
help_info = """Commande :
|
help_info = """Commande :
|
||||||
[1] RUN Exécuter et installer une instance
|
[1] Run - Exécuter et installer une instance
|
||||||
[2] EXEC Automatisation - Démonstration des fonctions développées
|
[2] Exec - Automatisation - Démonstration des fonctions développées
|
||||||
[3] UPD Mise à jour - Update all developed staging source code
|
[3] Mise à jour - Update all developed staging source code
|
||||||
[4] Code - Outil pour développeur
|
[4] Code - Outil pour développeur
|
||||||
|
[5] Doc - Recherche de documentation
|
||||||
|
[6] Database - Outils sur les bases de données
|
||||||
[0] Retour
|
[0] Retour
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -226,6 +237,14 @@ class TODO:
|
||||||
status = self.prompt_execute_code()
|
status = self.prompt_execute_code()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
|
elif status == "5":
|
||||||
|
status = self.prompt_execute_doc()
|
||||||
|
if status is not False:
|
||||||
|
return
|
||||||
|
elif status == "6":
|
||||||
|
status = self.prompt_execute_database()
|
||||||
|
if status is not False:
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
print("Commande non trouvée 🤖!")
|
print("Commande non trouvée 🤖!")
|
||||||
|
|
||||||
|
|
@ -234,13 +253,13 @@ class TODO:
|
||||||
|
|
||||||
first_installation_input = (
|
first_installation_input = (
|
||||||
input(
|
input(
|
||||||
"First system installation? This will process system installation"
|
"💬 First system installation? This will process system installation"
|
||||||
" before (Y/N): "
|
" before (Y/N): "
|
||||||
)
|
)
|
||||||
.strip()
|
.strip()
|
||||||
.upper()
|
.lower()
|
||||||
)
|
)
|
||||||
if first_installation_input == "Y":
|
if first_installation_input == "y":
|
||||||
cmd = "./script/version/update_env_version.py --install"
|
cmd = "./script/version/update_env_version.py --install"
|
||||||
self.executer_commande_live(cmd, source_erplibre=True)
|
self.executer_commande_live(cmd, source_erplibre=True)
|
||||||
print("Wait after OS installation before continue.")
|
print("Wait after OS installation before continue.")
|
||||||
|
|
@ -268,124 +287,106 @@ class TODO:
|
||||||
".idea"
|
".idea"
|
||||||
):
|
):
|
||||||
pycharm_configuration_input = (
|
pycharm_configuration_input = (
|
||||||
input("Open Pycharm? (Y/N): ").strip().upper()
|
input("💬 Open Pycharm? (Y/N): ").strip().lower()
|
||||||
)
|
)
|
||||||
if pycharm_configuration_input == "Y":
|
if pycharm_configuration_input == "y":
|
||||||
pycharm_bin = "pycharm" if has_pycharm else "pycharm-community"
|
pycharm_bin = "pycharm" if has_pycharm else "pycharm-community"
|
||||||
self.executer_commande_live(pycharm_bin, source_erplibre=True)
|
|
||||||
|
cmd = f"cd {os.getcwd()} && {pycharm_bin} ./"
|
||||||
|
self.executer_commande_live(
|
||||||
|
cmd,
|
||||||
|
source_erplibre=False,
|
||||||
|
single_source_erplibre=False,
|
||||||
|
new_window=True,
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
"Close Pycharm when processing is done before continue"
|
"👹 WAIT and Close Pycharm when processing is done before continue"
|
||||||
" this guide."
|
" this guide."
|
||||||
)
|
)
|
||||||
# Propose Odoo installation
|
|
||||||
# TODO detect last version supported
|
# TODO detect last version supported
|
||||||
odoo_installation_input = (
|
# cmd_intern = "./script/install/install_erplibre.sh"
|
||||||
input("Install virtual environment? (Y/N): ").strip().upper()
|
# TODO maybe update q to only install erplibre from install_locally
|
||||||
|
# TODO problem installing with q, the script depend on odoo
|
||||||
|
key_i = 0
|
||||||
|
dct_cmd_intern_begin = {
|
||||||
|
"q": (
|
||||||
|
"q",
|
||||||
|
"q: ERPLibre only with system python without Odoo",
|
||||||
|
"./script/install/install_erplibre.sh",
|
||||||
|
),
|
||||||
|
"w": (
|
||||||
|
"w",
|
||||||
|
"w: Install all Odoo version with ERPLibre",
|
||||||
|
"make install_odoo_all_version",
|
||||||
|
),
|
||||||
|
"0": (
|
||||||
|
"0",
|
||||||
|
"0: Quitter",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
dct_final_cmd_intern = {}
|
||||||
|
lst_version, lst_version_installed, odoo_installed_version = (
|
||||||
|
self.get_odoo_version()
|
||||||
)
|
)
|
||||||
if odoo_installation_input == "Y":
|
|
||||||
# cmd_intern = "./script/install/install_erplibre.sh"
|
|
||||||
# TODO maybe update q to only install erplibre from install_locally
|
|
||||||
# TODO problem installing with q, the script depend on odoo
|
|
||||||
key_i = 0
|
|
||||||
dct_cmd_intern_begin = {
|
|
||||||
"q": (
|
|
||||||
"q",
|
|
||||||
"q: ERPLibre only with system python without Odoo",
|
|
||||||
"./script/install/install_erplibre.sh",
|
|
||||||
),
|
|
||||||
"w": (
|
|
||||||
"w",
|
|
||||||
"w: Install all Odoo version with ERPLibre",
|
|
||||||
"make install_odoo_all_version",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
dct_final_cmd_intern = {}
|
|
||||||
with open(VERSION_DATA_FILE) as txt:
|
|
||||||
data_version = json.load(txt)
|
|
||||||
|
|
||||||
if not data_version:
|
for dct_version in lst_version[::-1]:
|
||||||
raise Exception(
|
key_i += 1
|
||||||
f"Internal error, no Odoo version is supported, please valide file '{VERSION_DATA_FILE}'"
|
key_s = str(key_i)
|
||||||
)
|
label = f"{key_s}: Odoo {dct_version.get('odoo_version')}"
|
||||||
|
|
||||||
lst_version_transform = []
|
odoo_version = f"odoo{dct_version.get('odoo_version')}"
|
||||||
for key, value in data_version.items():
|
if odoo_version in lst_version_installed:
|
||||||
lst_version_transform.append(value)
|
label += " - Installed"
|
||||||
value["erplibre_version"] = key
|
if odoo_version == odoo_installed_version:
|
||||||
|
label += " - Actual"
|
||||||
lst_version_installed = []
|
if dct_version.get("default"):
|
||||||
if os.path.exists(INSTALLED_ODOO_VERSION_FILE):
|
label += " - Default"
|
||||||
with open(INSTALLED_ODOO_VERSION_FILE) as txt:
|
if dct_version.get("is_deprecated"):
|
||||||
lst_version_installed = sorted(txt.read().splitlines())
|
label += " - Deprecated"
|
||||||
|
erplibre_version = dct_version.get("erplibre_version")
|
||||||
odoo_installed_version = None
|
dct_cmd_intern_begin[key_s] = (
|
||||||
if os.path.exists(ODOO_VERSION_FILE):
|
key_s,
|
||||||
with open(ODOO_VERSION_FILE) as txt:
|
label,
|
||||||
odoo_installed_version = f"odoo{txt.read().strip()}"
|
f"./script/version/update_env_version.py --erplibre_version {erplibre_version} --install_dev",
|
||||||
|
|
||||||
# Add odoo version installation on command
|
|
||||||
lst_version = sorted(
|
|
||||||
lst_version_transform, key=lambda k: k.get("erplibre_version")
|
|
||||||
)
|
)
|
||||||
for dct_version in lst_version[::-1]:
|
|
||||||
key_i += 1
|
|
||||||
key_s = str(key_i)
|
|
||||||
label = f"{key_s}: Odoo {dct_version.get('odoo_version')}"
|
|
||||||
|
|
||||||
odoo_version = f"odoo{dct_version.get('odoo_version')}"
|
# Add final command
|
||||||
if odoo_version in lst_version_installed:
|
dct_cmd_intern = {**dct_cmd_intern_begin, **dct_final_cmd_intern}
|
||||||
label += " - Installed"
|
|
||||||
if odoo_version == odoo_installed_version:
|
|
||||||
label += " - Actual"
|
|
||||||
if dct_version.get("default"):
|
|
||||||
label += " - Default"
|
|
||||||
if dct_version.get("is_deprecated"):
|
|
||||||
label += " - Deprecated"
|
|
||||||
erplibre_version = dct_version.get("erplibre_version")
|
|
||||||
dct_cmd_intern_begin[key_s] = (
|
|
||||||
key_s,
|
|
||||||
label,
|
|
||||||
f"./script/version/update_env_version.py --erplibre_version {erplibre_version} --install_dev",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add final command
|
# Show command
|
||||||
dct_cmd_intern = {**dct_cmd_intern_begin, **dct_final_cmd_intern}
|
odoo_version_input = ""
|
||||||
|
while odoo_version_input not in dct_cmd_intern.keys():
|
||||||
|
if odoo_version_input:
|
||||||
|
print(f"Error, cannot understand value '{odoo_version_input}'")
|
||||||
|
str_input_dyn_odoo_version = (
|
||||||
|
"💬 Choose a version:\n\t"
|
||||||
|
+ "\n\t".join([a[1] for a in dct_cmd_intern.values()])
|
||||||
|
+ "\nSelect : "
|
||||||
|
)
|
||||||
|
odoo_version_input = (
|
||||||
|
input(str_input_dyn_odoo_version).strip().lower()
|
||||||
|
)
|
||||||
|
|
||||||
# Show command
|
if odoo_version_input == "0":
|
||||||
odoo_version_input = ""
|
return
|
||||||
while odoo_version_input not in dct_cmd_intern.keys():
|
|
||||||
if odoo_version_input:
|
|
||||||
print(
|
|
||||||
f"Error, cannot understand value '{odoo_version_input}'"
|
|
||||||
)
|
|
||||||
str_input_dyn_odoo_version = (
|
|
||||||
"Choose a version:\n\t"
|
|
||||||
+ "\n\t".join([a[1] for a in dct_cmd_intern.values()])
|
|
||||||
+ "\nSelect : "
|
|
||||||
)
|
|
||||||
odoo_version_input = (
|
|
||||||
input(str_input_dyn_odoo_version).strip().lower()
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd_intern = dct_cmd_intern.get(odoo_version_input)[2]
|
cmd_intern = dct_cmd_intern.get(odoo_version_input)[2]
|
||||||
print(f"Will execute :\n{cmd_intern}")
|
print(f"Will execute :\n{cmd_intern}")
|
||||||
|
|
||||||
# TODO use external script to detect terminal to use on system
|
# TODO use external script to detect terminal to use on system
|
||||||
# TODO check script open_terminal_code_generator.sh
|
# TODO check script open_terminal_code_generator.sh
|
||||||
# cmd_extern = f"gnome-terminal -- bash -c '{cmd_intern};bash'"
|
# cmd_extern = f"gnome-terminal -- bash -c '{cmd_intern};bash'"
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
cmd_intern, shell=True, executable="/bin/bash", check=True
|
cmd_intern, shell=True, executable="/bin/bash", check=True
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(
|
print(
|
||||||
f"Le script Bash «{cmd_intern}» a échoué avec le code de retour {e.returncode}."
|
f"Le script Bash «{cmd_intern}» a échoué avec le code de retour {e.returncode}."
|
||||||
)
|
)
|
||||||
print("Wait after installation and open projects by terminal.")
|
print("Wait after installation and open projects by terminal.")
|
||||||
print("make open_terminal")
|
print("make open_terminal")
|
||||||
self.restart_script(str(e))
|
self.restart_script(str(e))
|
||||||
else:
|
|
||||||
print("Nothing to do, you need a fresh installation to continue.")
|
|
||||||
|
|
||||||
def execute_from_configuration(
|
def execute_from_configuration(
|
||||||
self, dct_instance, exec_run_db=False, ignore_makefile=False
|
self, dct_instance, exec_run_db=False, ignore_makefile=False
|
||||||
|
|
@ -407,7 +408,11 @@ class TODO:
|
||||||
|
|
||||||
makefile_cmd = dct_instance.get("makefile_cmd")
|
makefile_cmd = dct_instance.get("makefile_cmd")
|
||||||
if makefile_cmd and not ignore_makefile:
|
if makefile_cmd and not ignore_makefile:
|
||||||
self.executer_commande_live(f"make {makefile_cmd}")
|
self.executer_commande_live(
|
||||||
|
f"make {makefile_cmd}",
|
||||||
|
source_erplibre=False,
|
||||||
|
single_source_erplibre=True,
|
||||||
|
)
|
||||||
|
|
||||||
if exec_run_db:
|
if exec_run_db:
|
||||||
db_name = dct_instance.get("database")
|
db_name = dct_instance.get("database")
|
||||||
|
|
@ -435,7 +440,7 @@ class TODO:
|
||||||
# TODO proposer le déploiement à distance
|
# TODO proposer le déploiement à distance
|
||||||
# TODO proposer l'exécution de docker
|
# TODO proposer l'exécution de docker
|
||||||
# TODO proposer la création de docker
|
# TODO proposer la création de docker
|
||||||
lst_instance = self.get_config(["instance"])
|
lst_instance = self.config_file.get_config(["instance"])
|
||||||
help_info = self.fill_help_info(lst_instance)
|
help_info = self.fill_help_info(lst_instance)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -464,7 +469,7 @@ class TODO:
|
||||||
print("Commande non trouvée 🤖!")
|
print("Commande non trouvée 🤖!")
|
||||||
|
|
||||||
def prompt_execute_fonction(self):
|
def prompt_execute_fonction(self):
|
||||||
lst_instance = self.get_config(["function"])
|
lst_instance = self.config_file.get_config(["function"])
|
||||||
help_info = self.fill_help_info(lst_instance)
|
help_info = self.fill_help_info(lst_instance)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -493,8 +498,18 @@ class TODO:
|
||||||
# TODO proposer les modules manuelles selon la configuration à mettre à jour
|
# TODO proposer les modules manuelles selon la configuration à mettre à jour
|
||||||
# TODO proposer la mise à jour de l'IDE
|
# TODO proposer la mise à jour de l'IDE
|
||||||
# TODO proposer la mise à jour des git-repo
|
# TODO proposer la mise à jour des git-repo
|
||||||
|
# TODO faire la mise à jour de ERPLibre
|
||||||
|
# TODO faire l'upgrade d'un odoo vers un autre
|
||||||
|
|
||||||
lst_instance = self.get_config(["update_from_makefile"])
|
lst_instance = self.config_file.get_config(["update_from_makefile"])
|
||||||
|
dct_upgrade_odoo_database = {
|
||||||
|
"prompt_description": "Upgrade Odoo - Migration Database",
|
||||||
|
}
|
||||||
|
lst_instance.append(dct_upgrade_odoo_database)
|
||||||
|
dct_upgrade_poetry = {
|
||||||
|
"prompt_description": "Upgrade Poetry - Dependency of Odoo",
|
||||||
|
}
|
||||||
|
lst_instance.append(dct_upgrade_poetry)
|
||||||
help_info = self.fill_help_info(lst_instance)
|
help_info = self.fill_help_info(lst_instance)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -502,10 +517,15 @@ class TODO:
|
||||||
print()
|
print()
|
||||||
if status == "0":
|
if status == "0":
|
||||||
return False
|
return False
|
||||||
|
elif status == str(len(lst_instance) - 1):
|
||||||
|
upgrade = todo_upgrade.TodoUpgrade(self)
|
||||||
|
upgrade.execute_odoo_upgrade()
|
||||||
|
elif status == str(len(lst_instance)):
|
||||||
|
self.upgrade_poetry()
|
||||||
else:
|
else:
|
||||||
cmd_no_found = True
|
cmd_no_found = True
|
||||||
try:
|
try:
|
||||||
int_cmd = int(status)
|
int_cmd = int(status) - 1
|
||||||
if 0 < int_cmd <= len(lst_instance):
|
if 0 < int_cmd <= len(lst_instance):
|
||||||
cmd_no_found = False
|
cmd_no_found = False
|
||||||
dct_instance = lst_instance[int_cmd - 1]
|
dct_instance = lst_instance[int_cmd - 1]
|
||||||
|
|
@ -514,7 +534,6 @@ class TODO:
|
||||||
pass
|
pass
|
||||||
if cmd_no_found:
|
if cmd_no_found:
|
||||||
print("Commande non trouvée 🤖!")
|
print("Commande non trouvée 🤖!")
|
||||||
return False
|
|
||||||
|
|
||||||
def prompt_execute_code(self):
|
def prompt_execute_code(self):
|
||||||
print("🤖 Qu'avez-vous de besoin pour développer?")
|
print("🤖 Qu'avez-vous de besoin pour développer?")
|
||||||
|
|
@ -529,7 +548,12 @@ class TODO:
|
||||||
# [1] Status Git local et distant
|
# [1] Status Git local et distant
|
||||||
# [0] Retour
|
# [0] Retour
|
||||||
# """
|
# """
|
||||||
lst_instance = self.get_config(["code_from_makefile"])
|
|
||||||
|
lst_instance = self.config_file.get_config(["code_from_makefile"])
|
||||||
|
dct_upgrade_odoo_database = {
|
||||||
|
"prompt_description": "Upgrade Module",
|
||||||
|
}
|
||||||
|
lst_instance.append(dct_upgrade_odoo_database)
|
||||||
help_info = self.fill_help_info(lst_instance)
|
help_info = self.fill_help_info(lst_instance)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -537,6 +561,8 @@ class TODO:
|
||||||
print()
|
print()
|
||||||
if status == "0":
|
if status == "0":
|
||||||
return False
|
return False
|
||||||
|
elif status == str(len(lst_instance)):
|
||||||
|
self.upgrade_module()
|
||||||
else:
|
else:
|
||||||
cmd_no_found = True
|
cmd_no_found = True
|
||||||
try:
|
try:
|
||||||
|
|
@ -549,7 +575,107 @@ class TODO:
|
||||||
pass
|
pass
|
||||||
if cmd_no_found:
|
if cmd_no_found:
|
||||||
print("Commande non trouvée 🤖!")
|
print("Commande non trouvée 🤖!")
|
||||||
return False
|
|
||||||
|
def prompt_execute_doc(self):
|
||||||
|
print("🤖 Vous cherchez de la documentation?")
|
||||||
|
lst_instance = [
|
||||||
|
{"prompt_description": "Migration module coverage"},
|
||||||
|
{"prompt_description": "What change between version"},
|
||||||
|
{"prompt_description": "OCA guidelines"},
|
||||||
|
{"prompt_description": "OCA migration Odoo 19 milestone"},
|
||||||
|
]
|
||||||
|
help_info = self.fill_help_info(lst_instance)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
status = click.prompt(help_info)
|
||||||
|
print()
|
||||||
|
if status == "0":
|
||||||
|
return False
|
||||||
|
elif status == "1":
|
||||||
|
str_version = input(
|
||||||
|
"Select version to upgrade Odoo CE (5-17) : "
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
int_version = int(str_version)
|
||||||
|
print(
|
||||||
|
"https://oca.github.io/OpenUpgrade/coverage_analysis/modules"
|
||||||
|
f"{int_version * 10}-{(int_version + 1) * 10}.html"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
print(
|
||||||
|
"https://oca.github.io/OpenUpgrade/030_coverage_analysis.html"
|
||||||
|
)
|
||||||
|
elif status == "2":
|
||||||
|
str_version = input(
|
||||||
|
"Select version to show what change for Odoo CE version 8-18) : "
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
int_version = int(str_version)
|
||||||
|
print(
|
||||||
|
f"https://github.com/OCA/maintainer-tools/wiki/Migration-to-version-{int_version}.0"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
print("https://github.com/OCA/maintainer-tools/wiki")
|
||||||
|
elif status == "3":
|
||||||
|
print(
|
||||||
|
"https://github.com/OCA/odoo-community.org/blob/master/website/Contribution/CONTRIBUTING.rst"
|
||||||
|
)
|
||||||
|
elif status == "4":
|
||||||
|
print("https://github.com/OCA/maintainer-tools/issues/658")
|
||||||
|
else:
|
||||||
|
print("Commande non trouvée 🤖!")
|
||||||
|
|
||||||
|
def prompt_execute_database(self):
|
||||||
|
print("🤖 Faites des modifications sur les bases de données!")
|
||||||
|
lst_instance = [
|
||||||
|
{
|
||||||
|
"prompt_description": "Download database to create backup (.zip)"
|
||||||
|
},
|
||||||
|
{"prompt_description": "Restore from backup (.zip)"},
|
||||||
|
]
|
||||||
|
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.download_database_backup_cli()
|
||||||
|
elif status == "2":
|
||||||
|
self.restore_from_database()
|
||||||
|
else:
|
||||||
|
print("Commande non trouvée 🤖!")
|
||||||
|
|
||||||
|
def get_odoo_version(self):
|
||||||
|
with open(VERSION_DATA_FILE) as txt:
|
||||||
|
data_version = json.load(txt)
|
||||||
|
|
||||||
|
if not data_version:
|
||||||
|
raise Exception(
|
||||||
|
f"Internal error, no Odoo version is supported, please valide file '{VERSION_DATA_FILE}'"
|
||||||
|
)
|
||||||
|
lst_version_transform = []
|
||||||
|
for key, value in data_version.items():
|
||||||
|
lst_version_transform.append(value)
|
||||||
|
value["erplibre_version"] = key
|
||||||
|
|
||||||
|
lst_version_installed = []
|
||||||
|
if os.path.exists(INSTALLED_ODOO_VERSION_FILE):
|
||||||
|
with open(INSTALLED_ODOO_VERSION_FILE) as txt:
|
||||||
|
lst_version_installed = sorted(txt.read().splitlines())
|
||||||
|
|
||||||
|
odoo_installed_version = None
|
||||||
|
if os.path.exists(ODOO_VERSION_FILE):
|
||||||
|
with open(ODOO_VERSION_FILE) as txt:
|
||||||
|
odoo_installed_version = f"odoo{txt.read().strip()}"
|
||||||
|
|
||||||
|
# Add odoo version installation on command
|
||||||
|
lst_version = sorted(
|
||||||
|
lst_version_transform, key=lambda k: k.get("erplibre_version")
|
||||||
|
)
|
||||||
|
|
||||||
|
return lst_version, lst_version_installed, odoo_installed_version
|
||||||
|
|
||||||
def kdbx_get_extra_command_user(self, kdbx_key):
|
def kdbx_get_extra_command_user(self, kdbx_key):
|
||||||
lst_value = []
|
lst_value = []
|
||||||
|
|
@ -582,11 +708,17 @@ class TODO:
|
||||||
return lst_value
|
return lst_value
|
||||||
|
|
||||||
def prompt_execute_selenium_and_run_db(self, bd, extra_cmd_web_login=""):
|
def prompt_execute_selenium_and_run_db(self, bd, extra_cmd_web_login=""):
|
||||||
cmd = (
|
# cmd = (
|
||||||
f'parallel ::: "./run.sh -d {bd}" "sleep'
|
# f'parallel ::: "./run.sh -d {bd}" "sleep'
|
||||||
f' 3;./script/selenium/web_login.py{extra_cmd_web_login}"'
|
# f' 3;./script/selenium/web_login.py{extra_cmd_web_login}"'
|
||||||
|
# )
|
||||||
|
# self.executer_commande_live(cmd)
|
||||||
|
cmd_server = f"./run.sh -d {bd};bash"
|
||||||
|
self.executer_commande_live(cmd_server)
|
||||||
|
cmd_client = (
|
||||||
|
f"sleep 3;./script/selenium/web_login.py{extra_cmd_web_login};bash"
|
||||||
)
|
)
|
||||||
self.executer_commande_live(cmd)
|
self.executer_commande_live(cmd_client)
|
||||||
|
|
||||||
def prompt_execute_selenium(self, command=None, extra_cmd_web_login=""):
|
def prompt_execute_selenium(self, command=None, extra_cmd_web_login=""):
|
||||||
lst_cmd = []
|
lst_cmd = []
|
||||||
|
|
@ -609,7 +741,20 @@ class TODO:
|
||||||
new_cmd += f' "sleep {1 * i};{cmd}"'
|
new_cmd += f' "sleep {1 * i};{cmd}"'
|
||||||
self.executer_commande_live(new_cmd)
|
self.executer_commande_live(new_cmd)
|
||||||
|
|
||||||
def executer_commande_live(self, commande, source_erplibre=True):
|
def executer_commande_live(
|
||||||
|
self,
|
||||||
|
commande,
|
||||||
|
source_erplibre=True,
|
||||||
|
quiet=False,
|
||||||
|
single_source_erplibre=False,
|
||||||
|
new_window=False,
|
||||||
|
single_source_odoo=False,
|
||||||
|
source_odoo="",
|
||||||
|
new_env=None,
|
||||||
|
return_status_and_command=False,
|
||||||
|
return_status_and_output=False,
|
||||||
|
return_status_and_output_and_command=False,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Exécute une commande et affiche la sortie en direct.
|
Exécute une commande et affiche la sortie en direct.
|
||||||
|
|
||||||
|
|
@ -617,6 +762,12 @@ class TODO:
|
||||||
commande (str): La commande à exécuter (sous forme de chaîne de caractères).
|
commande (str): La commande à exécuter (sous forme de chaîne de caractères).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
my_env = os.environ.copy()
|
||||||
|
if new_env:
|
||||||
|
my_env.update(new_env)
|
||||||
|
|
||||||
|
process_start_time = time.time()
|
||||||
|
return_status = None
|
||||||
if source_erplibre:
|
if source_erplibre:
|
||||||
# commande = f"source ./{cst_venv_erplibre}/bin/activate && " + commande
|
# commande = f"source ./{cst_venv_erplibre}/bin/activate && " + commande
|
||||||
# cmd = (
|
# cmd = (
|
||||||
|
|
@ -624,8 +775,25 @@ class TODO:
|
||||||
# f" ./{cst_venv_erplibre}/bin/activate;{commande}'"
|
# f" ./{cst_venv_erplibre}/bin/activate;{commande}'"
|
||||||
# )
|
# )
|
||||||
commande = self.cmd_source_erplibre % commande
|
commande = self.cmd_source_erplibre % commande
|
||||||
print(f"Execute : {commande}")
|
|
||||||
# os.system(f"./script/terminal/open_terminal.sh {commande}")
|
# os.system(f"./script/terminal/open_terminal.sh {commande}")
|
||||||
|
elif single_source_erplibre:
|
||||||
|
commande = (
|
||||||
|
f"source ./{cst_venv_erplibre}/bin/activate && %s" % commande
|
||||||
|
)
|
||||||
|
elif single_source_odoo:
|
||||||
|
if not source_odoo and os.path.exists("./.erplibre-version"):
|
||||||
|
with open("./.erplibre-version") as f:
|
||||||
|
source_odoo = f.read()
|
||||||
|
commande = (
|
||||||
|
f"source ./.venv.{source_odoo}/bin/activate && {commande}"
|
||||||
|
)
|
||||||
|
elif new_window:
|
||||||
|
commande = self.cmd_source_default % commande
|
||||||
|
|
||||||
|
print("🏠 ⬇ Execute command :")
|
||||||
|
print(commande)
|
||||||
|
lst_output = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
commande,
|
commande,
|
||||||
|
|
@ -636,6 +804,7 @@ class TODO:
|
||||||
text=True,
|
text=True,
|
||||||
bufsize=1, # Désactive la mise en tampon pour la sortie en direct
|
bufsize=1, # Désactive la mise en tampon pour la sortie en direct
|
||||||
universal_newlines=True, # Pour traiter les sauts de lignes correctement
|
universal_newlines=True, # Pour traiter les sauts de lignes correctement
|
||||||
|
env=my_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -643,10 +812,15 @@ class TODO:
|
||||||
if not ligne:
|
if not ligne:
|
||||||
break
|
break
|
||||||
print(ligne, end="")
|
print(ligne, end="")
|
||||||
|
if (
|
||||||
|
return_status_and_output
|
||||||
|
or return_status_and_output_and_command
|
||||||
|
):
|
||||||
|
lst_output.append(ligne)
|
||||||
|
|
||||||
process.wait() # Attendre la fin du process
|
process.wait() # Attendre la fin du process
|
||||||
|
return_status = process.returncode
|
||||||
if process.returncode != 0:
|
if process.returncode != 0 and not quiet:
|
||||||
print(
|
print(
|
||||||
"La commande a retourné un code d'erreur :"
|
"La commande a retourné un code d'erreur :"
|
||||||
f" {process.returncode}"
|
f" {process.returncode}"
|
||||||
|
|
@ -664,6 +838,23 @@ class TODO:
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Une erreur s'est produite : {e}")
|
print(f"Une erreur s'est produite : {e}")
|
||||||
|
process_end_time = time.time()
|
||||||
|
duration_sec = process_end_time - process_start_time
|
||||||
|
if humanize:
|
||||||
|
duration_delta = datetime.timedelta(seconds=duration_sec)
|
||||||
|
humain_time = humanize.precisedelta(duration_delta)
|
||||||
|
print(f"🏠 ⬆ Executed ({humain_time}) :")
|
||||||
|
else:
|
||||||
|
print(f"🏠 ⬆ Executed ({duration_sec:.2f} sec.) :")
|
||||||
|
print(commande)
|
||||||
|
print()
|
||||||
|
if return_status_and_output_and_command:
|
||||||
|
return return_status, commande, lst_output
|
||||||
|
if return_status_and_command:
|
||||||
|
return return_status, commande
|
||||||
|
if return_status_and_output:
|
||||||
|
return return_status, lst_output
|
||||||
|
return return_status
|
||||||
|
|
||||||
def crash_diagnostic(self, e):
|
def crash_diagnostic(self, e):
|
||||||
# TODO show message at start if os.path.exists(file_error_path)
|
# TODO show message at start if os.path.exists(file_error_path)
|
||||||
|
|
@ -716,6 +907,7 @@ class TODO:
|
||||||
import click
|
import click
|
||||||
import humanize
|
import humanize
|
||||||
import openai
|
import openai
|
||||||
|
import urwid
|
||||||
from pykeepass import PyKeePass
|
from pykeepass import PyKeePass
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Rerun and exit")
|
print("Rerun and exit")
|
||||||
|
|
@ -725,6 +917,163 @@ class TODO:
|
||||||
else:
|
else:
|
||||||
self.prompt_install()
|
self.prompt_install()
|
||||||
|
|
||||||
|
def upgrade_module(self):
|
||||||
|
upgrade = todo_upgrade.TodoUpgrade(self)
|
||||||
|
upgrade.execute_module_upgrade()
|
||||||
|
|
||||||
|
def upgrade_poetry(self):
|
||||||
|
# Only show the version to the user
|
||||||
|
status = self.executer_commande_live(
|
||||||
|
f"make version",
|
||||||
|
source_erplibre=False,
|
||||||
|
)
|
||||||
|
# TODO maybe autodetect to update it
|
||||||
|
git_repo_update_input = input(
|
||||||
|
"💬 Would you like to fetch all your git repositories, you need it (y/Y) : "
|
||||||
|
)
|
||||||
|
if git_repo_update_input.strip().lower() == "y":
|
||||||
|
status = self.executer_commande_live(
|
||||||
|
f"./script/manifest/update_manifest_local_dev.sh",
|
||||||
|
source_erplibre=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
poetry_lock = "./poetry.lock"
|
||||||
|
try:
|
||||||
|
os.remove(poetry_lock)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
odoo_long_version = ""
|
||||||
|
if os.path.exists("./.erplibre-version"):
|
||||||
|
with open("./.erplibre-version") as f:
|
||||||
|
odoo_long_version = f.read()
|
||||||
|
path_file_odoo_lock = f"./requirement/poetry.{odoo_long_version}.lock"
|
||||||
|
if odoo_long_version:
|
||||||
|
try:
|
||||||
|
os.remove(path_file_odoo_lock)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
status = self.executer_commande_live(
|
||||||
|
f"pip install -r requirement/erplibre_require-ments-poetry.txt && "
|
||||||
|
f"./script/poetry/poetry_update.py -f",
|
||||||
|
source_erplibre=False,
|
||||||
|
single_source_erplibre=False,
|
||||||
|
single_source_odoo=True,
|
||||||
|
source_odoo=odoo_long_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
if os.path.exists(poetry_lock):
|
||||||
|
shutil.copy2(poetry_lock, path_file_odoo_lock)
|
||||||
|
|
||||||
|
def restore_from_database(self, show_remote_list=True):
|
||||||
|
path_image_db = os.path.join(os.getcwd(), "image_db")
|
||||||
|
print("[1] By filename from image_db")
|
||||||
|
print(f"[] Browser image_db {path_image_db}")
|
||||||
|
status = input("💬 Select : ")
|
||||||
|
if status == "1":
|
||||||
|
file_name = status
|
||||||
|
else:
|
||||||
|
self.dir_path = ""
|
||||||
|
|
||||||
|
file_browser = todo_file_browser.FileBrowser(
|
||||||
|
path_image_db, self.on_dir_selected
|
||||||
|
)
|
||||||
|
file_browser.run_main_frame()
|
||||||
|
file_name = os.path.basename(self.dir_path)
|
||||||
|
print(file_name)
|
||||||
|
|
||||||
|
database_name = input("💬 Database name : ")
|
||||||
|
if not database_name:
|
||||||
|
_logger.error("Missing database name")
|
||||||
|
return
|
||||||
|
status, lst_output = self.executer_commande_live(
|
||||||
|
f"python3 ./script/database/db_restore.py -d {database_name} --ignore_cache --image {file_name}",
|
||||||
|
return_status_and_output=True,
|
||||||
|
single_source_erplibre=True,
|
||||||
|
source_erplibre=False,
|
||||||
|
)
|
||||||
|
status = (
|
||||||
|
input("💬 Would you like to neutralize database (y/Y)? ")
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
if status == "y":
|
||||||
|
status, lst_output = self.executer_commande_live(
|
||||||
|
f"./script/addons/update_prod_to_dev.sh {database_name}",
|
||||||
|
return_status_and_output=True,
|
||||||
|
single_source_erplibre=True,
|
||||||
|
source_erplibre=False,
|
||||||
|
)
|
||||||
|
status = (
|
||||||
|
input("💬 Would you like to update all addons (y/Y)? ")
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
if status == "y":
|
||||||
|
status, lst_output = self.executer_commande_live(
|
||||||
|
f"./script/addons/update_addons_all.sh {database_name}",
|
||||||
|
return_status_and_output=True,
|
||||||
|
single_source_erplibre=True,
|
||||||
|
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:
|
||||||
|
status, lst_output = self.executer_commande_live(
|
||||||
|
f"python3 ./script/database/list_remote.py --raw --odoo-url {database_domain}",
|
||||||
|
return_status_and_output=True,
|
||||||
|
single_source_erplibre=True,
|
||||||
|
source_erplibre=False,
|
||||||
|
)
|
||||||
|
if len(lst_output) > 1:
|
||||||
|
for index, output in enumerate(lst_output):
|
||||||
|
print(f"{index + 1} - {output}")
|
||||||
|
database_name = input("Select id of database :").strip()
|
||||||
|
elif len(lst_output) == 1:
|
||||||
|
database_name = lst_output[0].strip()
|
||||||
|
else:
|
||||||
|
database_name = input(
|
||||||
|
"Cannot read remote database, Database name :\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
database_name = input("Database name :\n")
|
||||||
|
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%Hh%Mm%Ss")
|
||||||
|
default_output_path = f"./image_db/{database_name}_{timestamp}.zip"
|
||||||
|
output_path = input(
|
||||||
|
f"Output path (default: {default_output_path}) : "
|
||||||
|
).strip()
|
||||||
|
if not output_path:
|
||||||
|
output_path = default_output_path
|
||||||
|
|
||||||
|
master_password = getpass.getpass(prompt="Master password : ")
|
||||||
|
|
||||||
|
cmd = "script/database/download_remote.sh --quiet"
|
||||||
|
my_env = os.environ.copy()
|
||||||
|
my_env["MASTER_PWD"] = master_password
|
||||||
|
my_env["DATABASE_NAME"] = database_name
|
||||||
|
my_env["OUTPUT_FILE_PATH"] = output_path
|
||||||
|
my_env["ODOO_URL"] = database_domain
|
||||||
|
status, cmd_executed = self.executer_commande_live(
|
||||||
|
cmd,
|
||||||
|
source_erplibre=False,
|
||||||
|
return_status_and_command=True,
|
||||||
|
new_env=my_env,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(default_output_path, "r") as zip_ref:
|
||||||
|
manifest_file_1 = zip_ref.open("manifest.json")
|
||||||
|
_logger.info(
|
||||||
|
f"Log file '{default_output_path}' is complete and validated."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(e)
|
||||||
|
_logger.error(
|
||||||
|
f"Failed to read manifest.json from backup file '{default_output_path}'."
|
||||||
|
)
|
||||||
|
return status, output_path, database_name
|
||||||
|
|
||||||
def restart_script(self, last_error):
|
def restart_script(self, last_error):
|
||||||
print("Reboot TODO 🤖...")
|
print("Reboot TODO 🤖...")
|
||||||
# os.execv(sys.executable, ['python'] + sys.argv)
|
# os.execv(sys.executable, ['python'] + sys.argv)
|
||||||
|
|
@ -754,6 +1103,10 @@ class TODO:
|
||||||
print("Error detect at first execution.")
|
print("Error detect at first execution.")
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
def on_dir_selected(self, dir_path):
|
||||||
|
self.dir_path = dir_path
|
||||||
|
todo_file_browser.exit_program()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
|
||||||
97
script/todo/todo_file_browser.py
Normal file
97
script/todo/todo_file_browser.py
Normal 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 os
|
||||||
|
|
||||||
|
import urwid
|
||||||
|
|
||||||
|
|
||||||
|
class FileBrowser(urwid.WidgetWrap):
|
||||||
|
def __init__(self, initial_path, callback, open_dir=False):
|
||||||
|
self.callback = callback
|
||||||
|
self.current_path = os.path.abspath(initial_path)
|
||||||
|
self.list_walker = urwid.SimpleFocusListWalker([])
|
||||||
|
self.listbox = urwid.ListBox(self.list_walker)
|
||||||
|
super().__init__(self.listbox)
|
||||||
|
self.open_dir = open_dir
|
||||||
|
self.refresh_list()
|
||||||
|
|
||||||
|
def refresh_list(self):
|
||||||
|
"""Updates the list of files and directories."""
|
||||||
|
self.list_walker.clear()
|
||||||
|
self.list_walker.append(
|
||||||
|
urwid.Button("..", on_press=self.go_up_directory)
|
||||||
|
)
|
||||||
|
if self.open_dir:
|
||||||
|
self.list_walker.append(
|
||||||
|
urwid.Button(".", on_press=self.select_directory)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
entries = os.listdir(self.current_path)
|
||||||
|
entries.sort(key=lambda r: r.lower())
|
||||||
|
for entry in entries:
|
||||||
|
full_path = os.path.join(self.current_path, entry)
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
self.list_walker.append(
|
||||||
|
urwid.Button(f"{entry}/", on_press=self.open_directory)
|
||||||
|
)
|
||||||
|
elif not self.open_dir:
|
||||||
|
self.list_walker.append(
|
||||||
|
urwid.Button(entry, on_press=self.select_file)
|
||||||
|
)
|
||||||
|
except OSError as e:
|
||||||
|
# Handle directory access errors
|
||||||
|
self.list_walker.append(urwid.Text(f"Access Error: {e}"))
|
||||||
|
|
||||||
|
def go_up_directory(self, button):
|
||||||
|
"""Moves up one level in the directory hierarchy."""
|
||||||
|
parent_path = os.path.dirname(self.current_path)
|
||||||
|
if parent_path != self.current_path:
|
||||||
|
self.current_path = parent_path
|
||||||
|
self.refresh_list()
|
||||||
|
|
||||||
|
def open_directory(self, button):
|
||||||
|
"""Moves into a subdirectory."""
|
||||||
|
dirname = button.label[:-1]
|
||||||
|
new_path = os.path.join(self.current_path, dirname)
|
||||||
|
if os.path.isdir(new_path):
|
||||||
|
self.current_path = new_path
|
||||||
|
self.refresh_list()
|
||||||
|
|
||||||
|
def select_directory(self, button):
|
||||||
|
"""Selects a file and calls the callback function."""
|
||||||
|
self.callback(self.current_path)
|
||||||
|
|
||||||
|
def select_file(self, button):
|
||||||
|
"""Selects a file and calls the callback function."""
|
||||||
|
filename = button.label
|
||||||
|
selected_file_path = os.path.join(self.current_path, filename)
|
||||||
|
self.callback(selected_file_path)
|
||||||
|
|
||||||
|
def run_main_frame(self):
|
||||||
|
main_frame = urwid.Frame(
|
||||||
|
body=self,
|
||||||
|
header=urwid.Text(("header", f"Navigate: {self.current_path}")),
|
||||||
|
footer=urwid.Text(
|
||||||
|
("footer", "Use arrow keys to navigate and Enter to select.")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette = [
|
||||||
|
("header", "dark cyan", "black"),
|
||||||
|
("footer", "dark cyan", "black"),
|
||||||
|
("body", "white", "black"),
|
||||||
|
("button", "black", "dark cyan", "standout"),
|
||||||
|
("focus", "white", "dark green", "bold"),
|
||||||
|
("bold", "bold", "black"),
|
||||||
|
]
|
||||||
|
|
||||||
|
loop = urwid.MainLoop(main_frame, palette)
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
|
||||||
|
def exit_program():
|
||||||
|
"""Exits the program."""
|
||||||
|
raise urwid.ExitMainLoop()
|
||||||
2060
script/todo/todo_upgrade.py
Executable file
2060
script/todo/todo_upgrade.py
Executable file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue