[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
|
||||
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"
|
||||
|
||||
.PHONY: format_code_generator
|
||||
|
|
@ -212,6 +216,11 @@ repo_configure_group_code_generator:
|
|||
repo_show_status:
|
||||
.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
|
||||
.PHONY: repo_diff_manifest_production
|
||||
repo_diff_manifest_production:
|
||||
|
|
@ -299,7 +308,7 @@ i18n_generate_demo_portal:
|
|||
|
||||
.PHONY: 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 #
|
||||
|
|
|
|||
|
|
@ -107,4 +107,8 @@ docker_build_release_beta:
|
|||
# docker clean all
|
||||
.PHONY: 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
|
||||
|
|
|
|||
|
|
@ -118,4 +118,10 @@ test_addons_helpdesk:
|
|||
./.venv.erplibre/bin/coverage combine -a
|
||||
./.venv.erplibre/bin/coverage report -m
|
||||
./.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
|
||||
|
||||
## 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
|
||||
|
||||
### Search all duplicate file recursively in given directory
|
||||
|
|
@ -188,7 +201,8 @@ limit_memory_hard = 0
|
|||
### Error non-overlapping IPv4 address pool
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ services:
|
|||
# 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
|
||||
|
||||
|
|
|
|||
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 configparser
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -52,6 +53,16 @@ def get_config():
|
|||
action="store_true",
|
||||
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()
|
||||
return args
|
||||
|
||||
|
|
@ -60,6 +71,8 @@ def main():
|
|||
config = get_config()
|
||||
if config.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.read(config.config)
|
||||
|
|
@ -67,21 +80,30 @@ def main():
|
|||
if "addons_path" in config_parser["options"]:
|
||||
addons_path = config_parser["options"]["addons_path"]
|
||||
else:
|
||||
_logger.error(
|
||||
"Missing item 'addons_path' in section 'options' in"
|
||||
f" '{config.config}'"
|
||||
)
|
||||
msg = f"Missing item 'addons_path' in section 'options' in '{config.config}'"
|
||||
if not config.output_json:
|
||||
_logger.error(msg)
|
||||
else:
|
||||
print("{'error':%s}" % msg)
|
||||
return -1
|
||||
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
|
||||
|
||||
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_empty = defaultdict(list)
|
||||
lst_module_not_exist = []
|
||||
lst_error = []
|
||||
lst_exist = []
|
||||
lst_missing = []
|
||||
lst_duplicate = []
|
||||
|
||||
for module in lst_module:
|
||||
for path in lst_addons_path:
|
||||
|
|
@ -102,10 +124,11 @@ def main():
|
|||
is_good = False
|
||||
error_missing_module = True
|
||||
module_list = "'" + "', '".join(lst_module_not_exist) + "'"
|
||||
_logger.error(
|
||||
"Missing"
|
||||
f" module{'s' if len(lst_module_not_exist) > 1 else ''} {module_list}"
|
||||
)
|
||||
msg = f"Missing module{'s' if len(lst_module_not_exist) > 1 else ''} {module_list}"
|
||||
if not config.output_json:
|
||||
_logger.error(msg)
|
||||
else:
|
||||
lst_missing.extend(lst_module_not_exist)
|
||||
if dct_module_exist:
|
||||
for key, lst_value in dct_module_exist.items():
|
||||
is_print_value = False
|
||||
|
|
@ -113,20 +136,41 @@ def main():
|
|||
is_print_value = True
|
||||
is_good = False
|
||||
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:
|
||||
is_print_value = True
|
||||
if is_print_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:
|
||||
for key, lst_value in dct_module_exist_empty.items():
|
||||
module_list = "'" + "', '".join(lst_value) + "'"
|
||||
_logger.warning(
|
||||
"Found this directory, but missing __manifest__.py:"
|
||||
f" {module_list}"
|
||||
)
|
||||
msg = f"Found this directory, but missing __manifest__.py: {module_list}"
|
||||
if not config.output_json:
|
||||
_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 error_missing_module:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
Red='\033[0;31m' # Red
|
||||
Color_Off='\033[0m' # Text Reset
|
||||
Red='\033[0;31m' # Red
|
||||
Color_Off='\033[0m' # Text Reset
|
||||
|
||||
# This script will remove mail configuration, remove backup configuration, and force admin user to test/test
|
||||
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"
|
||||
exit 1
|
||||
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.",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
help="Specify database to delete, separate by coma",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
|
@ -39,6 +43,16 @@ def main():
|
|||
|
||||
out_db = execute_shell("./odoo_bin.sh db --list")
|
||||
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:
|
||||
if config.test_only and not (
|
||||
db_name in ("test",)
|
||||
|
|
@ -46,11 +60,16 @@ def main():
|
|||
or db_name.startswith("new_project_")
|
||||
):
|
||||
continue
|
||||
execute_shell(
|
||||
"./odoo_bin.sh db --drop --database"
|
||||
f" {db_name}"
|
||||
)
|
||||
print(f"{db_name} deleted")
|
||||
if lst_database_to_delete and db_name not in lst_database_to_delete:
|
||||
continue
|
||||
|
||||
cmd_end += f' "./odoo_bin.sh db --drop --database {db_name}"'
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,16 @@ SUGGESTION
|
|||
action="store_true",
|
||||
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()
|
||||
return args
|
||||
|
||||
|
|
@ -122,8 +132,10 @@ def main():
|
|||
arg = f"{arg_base} --drop --database {config.database}"
|
||||
out = check_output(arg.split(" ")).decode()
|
||||
print(out)
|
||||
if config.only_drop:
|
||||
return
|
||||
# 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(
|
||||
f"## Create cache {cache_database} from image"
|
||||
f" {config.image} ##"
|
||||
|
|
@ -135,13 +147,23 @@ def main():
|
|||
out = check_output(arg.split(" ")).decode()
|
||||
print(out)
|
||||
# Clone database
|
||||
_logger.info(
|
||||
f"## Clone cache {cache_database} to database {config.database} ##"
|
||||
)
|
||||
arg = (
|
||||
f"{arg_base} --clone --from_database"
|
||||
f" {cache_database} --database {config.database}"
|
||||
)
|
||||
if config.ignore_cache:
|
||||
_logger.info(
|
||||
f"## Restoring {config.image} to database {config.database} ##"
|
||||
)
|
||||
arg = (
|
||||
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()
|
||||
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):
|
||||
_logger.error(f"Missing file {filename_odoo_version}")
|
||||
sys.exit(1)
|
||||
with open(".odoo-version", "r") as f:
|
||||
with open(filename_odoo_version, "r") as f:
|
||||
config.odoo_version = f.readline()
|
||||
|
||||
# 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
|
||||
ODOO_VERSION=$(cat .odoo-version)
|
||||
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=(
|
||||
"${working_path}/"
|
||||
"${working_path}/"
|
||||
"${working_path}/addons.odoo${ODOO_VERSION}/ERPLibre_erplibre_addons"
|
||||
"${working_path}/addons.odoo${ODOO_VERSION}/TechnoLibre_odoo-code-generator"
|
||||
"${working_path}/addons.odoo${ODOO_VERSION}/TechnoLibre_odoo-code-generator-template"
|
||||
# "${working_path}/addons.odoo${ODOO_VERSION}/OCA_server-tools"
|
||||
"${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"
|
||||
|
||||
)
|
||||
|
||||
|
|
@ -16,7 +30,7 @@ first_iteration=true
|
|||
second_iteration=true
|
||||
if [[ "${OSTYPE}" == "linux-gnu" ]]; then
|
||||
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';"
|
||||
LONGCMD=""
|
||||
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
|
||||
for t in "${paths[@]}"; do
|
||||
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
|
||||
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
|
||||
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'"
|
||||
fi
|
||||
done
|
||||
osascript_command+=" -e 'end tell'"
|
||||
|
||||
echo "$osascript_command"
|
||||
# Exécution de la commande osascript
|
||||
eval "$osascript_command"
|
||||
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
|
||||
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",
|
||||
"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
|
||||
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import datetime
|
||||
import getpass
|
||||
|
|
@ -9,6 +11,14 @@ import shutil
|
|||
import subprocess
|
||||
import sys
|
||||
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"
|
||||
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(
|
||||
".repo", "installed_odoo_version.txt"
|
||||
)
|
||||
ODOO_VERSION_FILE = os.path.join(".odoo-version")
|
||||
ODOO_VERSION_FILE = ".odoo-version"
|
||||
ENABLE_CRASH = False
|
||||
CRASH_E = None
|
||||
|
||||
|
|
@ -27,12 +37,13 @@ try:
|
|||
import click
|
||||
import humanize
|
||||
import openai
|
||||
from pykeepass import PyKeePass
|
||||
import todo_file_browser
|
||||
|
||||
# TODO implement urwid to improve text user interface
|
||||
# import urwid
|
||||
# TODO implement rich for beautiful print and table
|
||||
# import rich
|
||||
import todo_upgrade
|
||||
from pykeepass import PyKeePass
|
||||
except ModuleNotFoundError as e:
|
||||
humanize = None
|
||||
ENABLE_CRASH = True
|
||||
|
|
@ -41,35 +52,48 @@ except ModuleNotFoundError as e:
|
|||
if not ENABLE_CRASH:
|
||||
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__)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
class TODO:
|
||||
def __init__(self):
|
||||
self.dir_path = None
|
||||
self.kdbx = None
|
||||
self.init()
|
||||
self.file_path = None
|
||||
self.config_file = config_file.ConfigFile()
|
||||
|
||||
def init(self):
|
||||
# Get command
|
||||
self.cmd_source_erplibre = ""
|
||||
self.cmd_source_default = ""
|
||||
exec_path_gnome_terminal = shutil.which("gnome-terminal")
|
||||
if exec_path_gnome_terminal:
|
||||
self.cmd_source_erplibre = (
|
||||
f"gnome-terminal -- bash -c 'source"
|
||||
f" ./{cst_venv_erplibre}/bin/activate;%s'"
|
||||
)
|
||||
self.cmd_source_default = "gnome-terminal -- bash -c '" f"%s'"
|
||||
else:
|
||||
exec_path_tell = shutil.which("osascript")
|
||||
if exec_path_tell:
|
||||
self.cmd_source_erplibre = (
|
||||
"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 += f"./{cst_venv_erplibre}/bin/activate; %s\" in front window'"
|
||||
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"cd {os.getcwd()}; source ./{cst_venv_erplibre}/bin/activate; %s\" in front window'"
|
||||
self.cmd_source_erplibre += " -e 'end tell'"
|
||||
else:
|
||||
self.cmd_source_erplibre = (
|
||||
|
|
@ -77,7 +101,7 @@ class TODO:
|
|||
)
|
||||
|
||||
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("Ouverture de TODO en cours ...")
|
||||
print("🤖 => Entre tes directives par son chiffre et fait Entrée!")
|
||||
|
|
@ -93,12 +117,14 @@ class TODO:
|
|||
status = click.prompt(help_info)
|
||||
except NameError:
|
||||
print("Do")
|
||||
print("source .venv.erplibre/bin/activate && make")
|
||||
print(f"source ./{cst_venv_erplibre}/bin/activate && make")
|
||||
sys.exit(1)
|
||||
except ImportError:
|
||||
print("Do")
|
||||
print("source .venv.erplibre/bin/activate && make")
|
||||
print(f"source ./{cst_venv_erplibre}/bin/activate && make")
|
||||
sys.exit(1)
|
||||
except click.exceptions.Abort:
|
||||
sys.exit(0)
|
||||
print()
|
||||
if status == "0":
|
||||
break
|
||||
|
|
@ -127,7 +153,7 @@ class TODO:
|
|||
if self.kdbx:
|
||||
return self.kdbx
|
||||
# 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:
|
||||
root = tk.Tk()
|
||||
root.withdraw() # Hide the main window
|
||||
|
|
@ -136,10 +162,12 @@ class TODO:
|
|||
filetypes=(("KeepassX files", "*.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
|
||||
|
||||
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:
|
||||
mot_de_passe_kdbx = getpass.getpass(
|
||||
prompt="Entrez votre mot de passe : "
|
||||
|
|
@ -151,25 +179,6 @@ class TODO:
|
|||
self.kdbx = 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):
|
||||
while True:
|
||||
help_info = """Commande :
|
||||
|
|
@ -182,7 +191,7 @@ class TODO:
|
|||
kp = self.get_kdbx()
|
||||
if not kp:
|
||||
return
|
||||
nom_configuration = self.get_config(
|
||||
nom_configuration = self.config_file.get_config(
|
||||
["kdbx_config", "openai", "kdbx_key"]
|
||||
)
|
||||
entry = kp.find_entries_by_title(nom_configuration, first=True)
|
||||
|
|
@ -199,10 +208,12 @@ class TODO:
|
|||
|
||||
def prompt_execute(self):
|
||||
help_info = """Commande :
|
||||
[1] RUN Exécuter et installer une instance
|
||||
[2] EXEC Automatisation - Démonstration des fonctions développées
|
||||
[3] UPD Mise à jour - Update all developed staging source code
|
||||
[1] Run - Exécuter et installer une instance
|
||||
[2] Exec - Automatisation - Démonstration des fonctions développées
|
||||
[3] Mise à jour - Update all developed staging source code
|
||||
[4] Code - Outil pour développeur
|
||||
[5] Doc - Recherche de documentation
|
||||
[6] Database - Outils sur les bases de données
|
||||
[0] Retour
|
||||
"""
|
||||
while True:
|
||||
|
|
@ -226,6 +237,14 @@ class TODO:
|
|||
status = self.prompt_execute_code()
|
||||
if status is not False:
|
||||
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:
|
||||
print("Commande non trouvée 🤖!")
|
||||
|
||||
|
|
@ -234,13 +253,13 @@ class TODO:
|
|||
|
||||
first_installation_input = (
|
||||
input(
|
||||
"First system installation? This will process system installation"
|
||||
"💬 First system installation? This will process system installation"
|
||||
" before (Y/N): "
|
||||
)
|
||||
.strip()
|
||||
.upper()
|
||||
.lower()
|
||||
)
|
||||
if first_installation_input == "Y":
|
||||
if first_installation_input == "y":
|
||||
cmd = "./script/version/update_env_version.py --install"
|
||||
self.executer_commande_live(cmd, source_erplibre=True)
|
||||
print("Wait after OS installation before continue.")
|
||||
|
|
@ -268,124 +287,106 @@ class TODO:
|
|||
".idea"
|
||||
):
|
||||
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"
|
||||
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(
|
||||
"Close Pycharm when processing is done before continue"
|
||||
"👹 WAIT and Close Pycharm when processing is done before continue"
|
||||
" this guide."
|
||||
)
|
||||
# Propose Odoo installation
|
||||
# TODO detect last version supported
|
||||
odoo_installation_input = (
|
||||
input("Install virtual environment? (Y/N): ").strip().upper()
|
||||
# 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",
|
||||
),
|
||||
"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:
|
||||
raise Exception(
|
||||
f"Internal error, no Odoo version is supported, please valide file '{VERSION_DATA_FILE}'"
|
||||
)
|
||||
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')}"
|
||||
|
||||
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")
|
||||
odoo_version = f"odoo{dct_version.get('odoo_version')}"
|
||||
if odoo_version in lst_version_installed:
|
||||
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",
|
||||
)
|
||||
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')}"
|
||||
if odoo_version in lst_version_installed:
|
||||
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
|
||||
dct_cmd_intern = {**dct_cmd_intern_begin, **dct_final_cmd_intern}
|
||||
|
||||
# Add final command
|
||||
dct_cmd_intern = {**dct_cmd_intern_begin, **dct_final_cmd_intern}
|
||||
# Show command
|
||||
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
|
||||
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()
|
||||
)
|
||||
if odoo_version_input == "0":
|
||||
return
|
||||
|
||||
cmd_intern = dct_cmd_intern.get(odoo_version_input)[2]
|
||||
print(f"Will execute :\n{cmd_intern}")
|
||||
cmd_intern = dct_cmd_intern.get(odoo_version_input)[2]
|
||||
print(f"Will execute :\n{cmd_intern}")
|
||||
|
||||
# TODO use external script to detect terminal to use on system
|
||||
# TODO check script open_terminal_code_generator.sh
|
||||
# cmd_extern = f"gnome-terminal -- bash -c '{cmd_intern};bash'"
|
||||
try:
|
||||
subprocess.run(
|
||||
cmd_intern, shell=True, executable="/bin/bash", check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(
|
||||
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("make open_terminal")
|
||||
self.restart_script(str(e))
|
||||
else:
|
||||
print("Nothing to do, you need a fresh installation to continue.")
|
||||
# TODO use external script to detect terminal to use on system
|
||||
# TODO check script open_terminal_code_generator.sh
|
||||
# cmd_extern = f"gnome-terminal -- bash -c '{cmd_intern};bash'"
|
||||
try:
|
||||
subprocess.run(
|
||||
cmd_intern, shell=True, executable="/bin/bash", check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(
|
||||
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("make open_terminal")
|
||||
self.restart_script(str(e))
|
||||
|
||||
def execute_from_configuration(
|
||||
self, dct_instance, exec_run_db=False, ignore_makefile=False
|
||||
|
|
@ -407,7 +408,11 @@ class TODO:
|
|||
|
||||
makefile_cmd = dct_instance.get("makefile_cmd")
|
||||
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:
|
||||
db_name = dct_instance.get("database")
|
||||
|
|
@ -435,7 +440,7 @@ class TODO:
|
|||
# TODO proposer le déploiement à distance
|
||||
# TODO proposer l'exécution 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)
|
||||
|
||||
while True:
|
||||
|
|
@ -464,7 +469,7 @@ class TODO:
|
|||
print("Commande non trouvée 🤖!")
|
||||
|
||||
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)
|
||||
|
||||
while True:
|
||||
|
|
@ -493,8 +498,18 @@ class TODO:
|
|||
# TODO proposer les modules manuelles selon la configuration à mettre à jour
|
||||
# TODO proposer la mise à jour de l'IDE
|
||||
# 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)
|
||||
|
||||
while True:
|
||||
|
|
@ -502,10 +517,15 @@ class TODO:
|
|||
print()
|
||||
if status == "0":
|
||||
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:
|
||||
cmd_no_found = True
|
||||
try:
|
||||
int_cmd = int(status)
|
||||
int_cmd = int(status) - 1
|
||||
if 0 < int_cmd <= len(lst_instance):
|
||||
cmd_no_found = False
|
||||
dct_instance = lst_instance[int_cmd - 1]
|
||||
|
|
@ -514,7 +534,6 @@ class TODO:
|
|||
pass
|
||||
if cmd_no_found:
|
||||
print("Commande non trouvée 🤖!")
|
||||
return False
|
||||
|
||||
def prompt_execute_code(self):
|
||||
print("🤖 Qu'avez-vous de besoin pour développer?")
|
||||
|
|
@ -529,7 +548,12 @@ class TODO:
|
|||
# [1] Status Git local et distant
|
||||
# [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)
|
||||
|
||||
while True:
|
||||
|
|
@ -537,6 +561,8 @@ class TODO:
|
|||
print()
|
||||
if status == "0":
|
||||
return False
|
||||
elif status == str(len(lst_instance)):
|
||||
self.upgrade_module()
|
||||
else:
|
||||
cmd_no_found = True
|
||||
try:
|
||||
|
|
@ -549,7 +575,107 @@ class TODO:
|
|||
pass
|
||||
if cmd_no_found:
|
||||
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):
|
||||
lst_value = []
|
||||
|
|
@ -582,11 +708,17 @@ class TODO:
|
|||
return lst_value
|
||||
|
||||
def prompt_execute_selenium_and_run_db(self, bd, extra_cmd_web_login=""):
|
||||
cmd = (
|
||||
f'parallel ::: "./run.sh -d {bd}" "sleep'
|
||||
f' 3;./script/selenium/web_login.py{extra_cmd_web_login}"'
|
||||
# cmd = (
|
||||
# f'parallel ::: "./run.sh -d {bd}" "sleep'
|
||||
# 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=""):
|
||||
lst_cmd = []
|
||||
|
|
@ -609,7 +741,20 @@ class TODO:
|
|||
new_cmd += f' "sleep {1 * i};{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.
|
||||
|
||||
|
|
@ -617,6 +762,12 @@ class TODO:
|
|||
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:
|
||||
# commande = f"source ./{cst_venv_erplibre}/bin/activate && " + commande
|
||||
# cmd = (
|
||||
|
|
@ -624,8 +775,25 @@ class TODO:
|
|||
# f" ./{cst_venv_erplibre}/bin/activate;{commande}'"
|
||||
# )
|
||||
commande = self.cmd_source_erplibre % commande
|
||||
print(f"Execute : {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:
|
||||
process = subprocess.Popen(
|
||||
commande,
|
||||
|
|
@ -636,6 +804,7 @@ class TODO:
|
|||
text=True,
|
||||
bufsize=1, # Désactive la mise en tampon pour la sortie en direct
|
||||
universal_newlines=True, # Pour traiter les sauts de lignes correctement
|
||||
env=my_env,
|
||||
)
|
||||
|
||||
while True:
|
||||
|
|
@ -643,10 +812,15 @@ class TODO:
|
|||
if not ligne:
|
||||
break
|
||||
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
|
||||
|
||||
if process.returncode != 0:
|
||||
return_status = process.returncode
|
||||
if process.returncode != 0 and not quiet:
|
||||
print(
|
||||
"La commande a retourné un code d'erreur :"
|
||||
f" {process.returncode}"
|
||||
|
|
@ -664,6 +838,23 @@ class TODO:
|
|||
)
|
||||
except Exception as 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):
|
||||
# TODO show message at start if os.path.exists(file_error_path)
|
||||
|
|
@ -716,6 +907,7 @@ class TODO:
|
|||
import click
|
||||
import humanize
|
||||
import openai
|
||||
import urwid
|
||||
from pykeepass import PyKeePass
|
||||
except ImportError:
|
||||
print("Rerun and exit")
|
||||
|
|
@ -725,6 +917,163 @@ class TODO:
|
|||
else:
|
||||
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):
|
||||
print("Reboot TODO 🤖...")
|
||||
# os.execv(sys.executable, ['python'] + sys.argv)
|
||||
|
|
@ -754,6 +1103,10 @@ class TODO:
|
|||
print("Error detect at first execution.")
|
||||
print(e)
|
||||
|
||||
def on_dir_selected(self, dir_path):
|
||||
self.dir_path = dir_path
|
||||
todo_file_browser.exit_program()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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