erplibre/script/todo/todo.py
Mathieu Benoit 438edc3694 [IMP] script: sort execute menu alphabetically
Menu items were in arbitrary order making it hard to
find specific tools as the list grew to 14 entries.

Generated by Claude Code 2.1.72 model claude-sonnet-4-6

Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-03-11 04:04:17 -04:00

1667 lines
57 KiB
Python
Executable file

#!/usr/bin/env python3
# © 2021-2025 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import configparser
import datetime
import getpass
import json
import logging
import os
import shutil
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
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
from script.execute import execute
from script.todo.database_manager import DatabaseManager
from script.todo.kdbx_manager import KdbxManager
from script.todo.todo_i18n import get_lang, lang_is_configured, set_lang, t
from script.todo.version_manager import get_odoo_version
ERROR_LOG_PATH = ".erplibre.error.txt"
VENV_ERPLIBRE = ".venv.erplibre"
ENABLE_CRASH = False
CRASH_E = None
# Support mobile ERPLibre
ANDROID_DIR = "android"
MOBILE_HOME_PATH = "./mobile/erplibre_home_mobile"
STRINGS_FILE = os.path.join(
MOBILE_HOME_PATH, ANDROID_DIR, "app/src/main/res/values/strings.xml"
)
GRADLE_FILE = os.path.join(MOBILE_HOME_PATH, ANDROID_DIR, "app/build.gradle")
try:
import tkinter as tk
from tkinter import filedialog
import click
import dotenv
import humanize
import openai
import todo_file_browser
# 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
CRASH_E = e
if not ENABLE_CRASH:
print(t("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/todo.json"
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
class TODO:
def __init__(self):
self.dir_path = None
self.selected_file_path = None
self.config_file = config_file.ConfigFile()
self.execute = execute.Execute()
self.kdbx_manager = KdbxManager(self.config_file)
self.db_manager = DatabaseManager(self.execute, self.fill_help_info)
def _ask_language(self):
if not lang_is_configured():
print()
print("Choisir la langue / Choose language:")
print("[1] Francais")
print("[2] English")
choice = ""
while choice not in ("1", "2"):
choice = input("Select / Choisir : ").strip()
if choice == "1":
set_lang("fr")
else:
set_lang("en")
def _change_language(self):
print()
print(t("lang_prompt") + ":")
print(f"[1] {t('lang_french')}")
print(f"[2] {t('lang_english')}")
print(f"[0] {t('back')}")
choice = ""
while choice not in ("0", "1", "2"):
choice = input(t("selection")).strip()
if choice == "0":
return False
elif choice == "1":
set_lang("fr")
else:
set_lang("en")
print(t("lang_changed"))
def run(self):
with open(self.config_file.get_logo_ascii_file_path()) as my_file:
print(my_file.read())
self._ask_language()
print(t("opening"))
print(f"🤖 {t('enter_directives')}")
help_info = f"""{t("command")}
[1] {t("menu_execute")}
[2] {t("menu_install")}
[3] {t("menu_question")}
[4] {t("menu_fork")}
[0] {t("menu_quit")}
"""
while True:
try:
status = click.prompt(help_info)
except NameError:
print("Do")
print(f"source ./{VENV_ERPLIBRE}/bin/activate && make")
sys.exit(1)
except ImportError:
print("Do")
print(f"source ./{VENV_ERPLIBRE}/bin/activate && make")
sys.exit(1)
except click.exceptions.Abort:
sys.exit(0)
print()
if status == "0":
break
elif status == "1":
self.prompt_execute()
elif status == "2":
self.prompt_install()
elif status == "3":
self.execute_prompt_ia()
elif status == "4":
# cmd = (
# f"gnome-terminal --tab -- bash -c 'source"
# f" ./{VENV_ERPLIBRE}/bin/activate;make todo'"
# )
cmd = "make todo"
self.execute.exec_command_live(cmd, source_erplibre=True)
# elif status == "3" or status == "install":
# print("install")
else:
print(t("cmd_not_found"))
print(status)
# manipuler()
def execute_prompt_ia(self):
while True:
help_info = f"""{t("command")}
[0] {t("back")}
{t("ia_prompt")}"""
status = click.prompt(help_info)
print()
if status == "0":
return
kp = self.kdbx_manager.get_kdbx()
if not kp:
return
config_name = self.config_file.get_config_value(
["kdbx_config", "openai", "kdbx_key"]
)
entry = kp.find_entries_by_title(config_name, first=True)
client = openai.OpenAI(api_key=entry.password)
prompt_update = status
completion = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt_update}],
)
print(completion.choices[0].message.content)
print()
def prompt_execute(self):
help_info = f"""{t("command")}
[1] {t("menu_automation")}
[2] {t("menu_code")}
[3] {t("menu_config")}
[4] {t("menu_database")}
[5] {t("menu_doc")}
[6] {t("menu_git")}
[7] {t("menu_gpt_code")}
[8] {t("menu_lang")}
[9] {t("menu_network")}
[10] {t("menu_process")}
[11] {t("menu_run")}
[12] {t("menu_security")}
[13] {t("menu_test")}
[14] {t("menu_update")}
[0] {t("back")}
"""
while True:
status = click.prompt(help_info)
print()
if status == "0":
return
elif status == "1":
status = self.prompt_execute_function()
if status is not False:
return
elif status == "2":
status = self.prompt_execute_code()
if status is not False:
return
elif status == "3":
status = self.prompt_execute_config()
if status is not False:
return
elif status == "4":
status = self.prompt_execute_database()
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_git()
if status is not False:
return
elif status == "7":
status = self.prompt_execute_gpt_code()
if status is not False:
return
elif status == "8":
status = self._change_language()
if status is not False:
return
elif status == "9":
status = self.prompt_execute_network()
if status is not False:
return
elif status == "10":
status = self.prompt_execute_process()
if status is not False:
return
elif status == "11":
status = self.prompt_execute_instance()
if status is not False:
return
elif status == "12":
status = self.prompt_execute_security()
if status is not False:
return
elif status == "13":
status = self.prompt_execute_test()
if status is not False:
return
elif status == "14":
status = self.prompt_execute_update()
if status is not False:
return
else:
print(t("cmd_not_found"))
def prompt_install(self):
print("Detect first installation from code source.")
first_installation_input = (
input(
"💬 First system installation? This will process system installation"
" before (Y/N): "
)
.strip()
.lower()
)
if first_installation_input == "y":
cmd = "./script/version/update_env_version.py --install"
self.execute.exec_command_live(cmd, source_erplibre=True)
print("Wait after OS installation before continue.")
# First detect pycharm, need to be open before installation and close to increase speed
has_pycharm = False
has_pycharm_community = False
result = subprocess.run(
["which", "pycharm"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if result.returncode == 0:
has_pycharm = True
else:
result = subprocess.run(
["which", "pycharm-community"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
has_pycharm_community = result.returncode == 0
if (has_pycharm or has_pycharm_community) and not os.path.exists(
".idea"
):
pycharm_configuration_input = (
input("💬 Open Pycharm? (Y/N): ").strip().lower()
)
if pycharm_configuration_input == "y":
pycharm_bin = "pycharm" if has_pycharm else "pycharm-community"
cmd = f"cd {os.getcwd()} && {pycharm_bin} ./"
self.execute.exec_command_live(
cmd,
source_erplibre=False,
single_source_erplibre=False,
new_window=True,
)
print(
"👹 WAIT and Close Pycharm when processing is done before continue"
" this guide."
)
# TODO detect last version supported
# 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
commands_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",
),
"m": (
"m",
"m: ERPLibre with mobile home",
"./mobile/install_and_run.sh",
),
"0": (
"0",
f"0: {t('menu_quit')}",
),
}
commands_end = {}
versions, installed_versions, odoo_installed_version = (
get_odoo_version()
)
for version_info in versions[::-1]:
key_i += 1
key_s = str(key_i)
label = f"{key_s}: Odoo {version_info.get('odoo_version')}"
odoo_version = f"odoo{version_info.get('odoo_version')}"
if odoo_version in installed_versions:
label += " - Installed"
if odoo_version == odoo_installed_version:
label += " - Actual"
if version_info.get("default"):
label += " - Default"
if version_info.get("is_deprecated"):
label += " - Deprecated"
erplibre_version = version_info.get("erplibre_version")
commands_begin[key_s] = (
key_s,
label,
f"./script/version/update_env_version.py --erplibre_version {erplibre_version} --install_dev",
)
# Add final command
install_commands = {**commands_begin, **commands_end}
# Show command
odoo_version_input = ""
while odoo_version_input not in install_commands:
if odoo_version_input:
print(f"{t('error_value')} '{odoo_version_input}'")
str_input_dyn_odoo_version = (
f"💬 {t('choose_version')}\n\t"
+ "\n\t".join([a[1] for a in install_commands.values()])
+ f"\n{t('selection')}"
)
odoo_version_input = (
input(str_input_dyn_odoo_version).strip().lower()
)
if odoo_version_input == "0":
return
cmd_intern = install_commands.get(odoo_version_input)[2]
print(f"{t('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"{t('script_failed')} {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, instance, exec_run_db=False, ignore_makefile=False
):
# exec_run_db need argument database
kdbx_key = instance.get("kdbx_key")
odoo_user = instance.get("user")
odoo_password = instance.get("password")
if kdbx_key:
extra_cmd_web_login = self.kdbx_manager.get_extra_command_user(
kdbx_key
)
elif odoo_user and odoo_password:
extra_cmd_web_login = (
f" --default_email_auth {odoo_user} --default_password_auth"
f" '{odoo_password}'"
)
else:
extra_cmd_web_login = ""
makefile_cmd = instance.get("makefile_cmd")
if makefile_cmd and not ignore_makefile:
status = self.execute.exec_command_live(
f"make {makefile_cmd}",
source_erplibre=False,
single_source_erplibre=True,
)
if status:
_logger.error(
f"Status {status} - exit execute_from_configuration"
)
return
if exec_run_db:
db_name = instance.get("database")
self.prompt_execute_selenium_and_run_db(
db_name, extra_cmd_web_login=extra_cmd_web_login
)
command = instance.get("command")
if command:
self.prompt_execute_selenium(
command=command, extra_cmd_web_login=extra_cmd_web_login
)
callback = instance.get("callback")
if callback:
callback(instance)
def fill_help_info(self, choices):
help_info = t("command") + "\n"
help_end = f"[0] {t('back')}\n"
for i, instance in enumerate(choices):
desc_key = instance.get("prompt_description_key")
if desc_key:
desc = t(desc_key)
else:
desc = instance["prompt_description"]
help_info += f"[{i + 1}] " + desc + "\n"
help_info += help_end
return help_info
def prompt_execute_instance(self):
# TODO proposer le déploiement à distance
# TODO proposer l'exécution de docker
# TODO proposer la création de docker
choices = self.config_file.get_config("instance")
init_len = len(choices)
# Support mobile ERPLibre
if os.path.exists(MOBILE_HOME_PATH):
menu_entry = {
"prompt_description": t("mobile_compile_run"),
"callback": self.callback_make_mobile_home,
}
choices.append(menu_entry)
# Support custom database to execute
menu_entry = {
"prompt_description": t("choose_database"),
"callback": self.callback_execute_custom_database,
}
choices.insert(0, menu_entry)
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
else:
cmd_no_found = True
try:
int_cmd = int(status)
if 1 < int_cmd <= init_len:
cmd_no_found = False
status = click.confirm(t("new_instance_confirm"))
instance = choices[int_cmd - 1]
self.execute_from_configuration(
instance,
exec_run_db=True,
ignore_makefile=not bool(status),
)
elif int_cmd <= len(choices) or 1 == int_cmd:
cmd_no_found = False
# Execute dynamic instance
instance = choices[int_cmd - 1]
self.execute_from_configuration(
instance,
)
except ValueError:
pass
if cmd_no_found:
print(t("cmd_not_found"))
def prompt_execute_function(self):
choices = self.config_file.get_config("function")
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
else:
cmd_no_found = True
try:
int_cmd = int(status)
if 0 < int_cmd <= len(choices):
cmd_no_found = False
instance = choices[int_cmd - 1]
self.execute_from_configuration(instance)
except ValueError:
pass
if cmd_no_found:
print(t("cmd_not_found"))
def prompt_execute_update(self):
# self.execute.exec_command_live(f"make {makefile_cmd}")
print(f"🤖 {t('update_dev')}")
# TODO détecter les modules en modification pour faire la mise à jour en cours
# TODO demander sur quel BD faire la mise à jour
# 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
choices = self.config_file.get_config("update_from_makefile")
menu_entry = {
"prompt_description": t("upgrade_odoo_migration"),
}
choices.append(menu_entry)
poetry_entry = {
"prompt_description": t("upgrade_poetry_dependency"),
}
choices.append(poetry_entry)
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == str(len(choices) - 1):
upgrade = todo_upgrade.TodoUpgrade(self)
upgrade.execute_odoo_upgrade()
elif status == str(len(choices)):
self.upgrade_poetry()
else:
cmd_no_found = True
try:
int_cmd = int(status) - 1
if 0 < int_cmd <= len(choices):
cmd_no_found = False
instance = choices[int_cmd - 1]
self.execute_from_configuration(instance)
except ValueError:
pass
if cmd_no_found:
print(t("cmd_not_found"))
def prompt_execute_code(self):
print(f"🤖 {t('code_need')}")
# help_info = """Commande :
# [1] Status Git local et distant
# [2] Démarrer le générateur de code
# [3] Format - Formatage automatique selon changement [ou manuelle]
# [4] Qualité - Qualité logiciel, détecter les fichiers qui manquent les licences AGPLv3
# [0] Retour
# """
# help_info = """Commande :
# [1] Status Git local et distant
# [0] Retour
# """
choices = self.config_file.get_config("code_from_makefile")
menu_entry = {
"prompt_description": t("open_shell"),
}
choices.append(menu_entry)
menu_entry = {
"prompt_description": t("upgrade_module"),
}
choices.append(menu_entry)
choices.append(
{
"prompt_description": t("debug"),
}
)
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == str(len(choices)):
self.debug_ide()
elif status == str(len(choices) - 1):
self.upgrade_module()
elif status == str(len(choices) - 2):
self.open_shell_on_database()
else:
cmd_no_found = True
try:
int_cmd = int(status)
if 0 < int_cmd <= len(choices):
cmd_no_found = False
instance = choices[int_cmd - 1]
self.execute_from_configuration(instance)
except ValueError:
pass
if cmd_no_found:
print(t("cmd_not_found"))
def prompt_execute_git(self):
print(f"🤖 {t('git_manage')}")
choices = [
{"prompt_description": t("git_local_server")},
]
# Append config-driven entries
config_entries = self.config_file.get_config("git_from_makefile")
if config_entries:
choices.extend(config_entries)
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.prompt_execute_git_local_server()
else:
cmd_no_found = True
try:
int_cmd = int(status)
if 0 < int_cmd <= len(choices):
cmd_no_found = False
instance = choices[int_cmd - 1]
self.execute_from_configuration(instance)
except ValueError:
pass
if cmd_no_found:
print(t("cmd_not_found"))
def prompt_execute_git_local_server(self):
print(f"🤖 {t('git_repo_manage')}")
choices = [
{"prompt_description": t("git_repo_deploy_local")},
{"prompt_description": t("git_repo_deploy_production")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self._prompt_git_server_actions(production_ready=False)
elif status == "2":
self._prompt_git_server_actions(production_ready=True)
else:
print(t("cmd_not_found"))
def _prompt_git_server_actions(self, production_ready=False):
mode = (
t("git_mode_production")
if production_ready
else t("git_mode_local")
)
print(f"🤖 {mode}")
choices = [
{"prompt_description": t("git_action_all")},
{"prompt_description": t("git_action_init")},
{"prompt_description": t("git_action_remote")},
{"prompt_description": t("git_action_push")},
{"prompt_description": t("git_action_serve")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self._deploy_git_server(
production_ready=production_ready,
action="all",
)
elif status == "2":
self._deploy_git_server(
production_ready=production_ready,
action="init",
)
elif status == "3":
self._deploy_git_server(
production_ready=production_ready,
action="remote",
)
elif status == "4":
self._deploy_git_server(
production_ready=production_ready,
action="push",
)
elif status == "5":
self._deploy_git_server(
production_ready=production_ready,
action="serve",
)
else:
print(t("cmd_not_found"))
def _deploy_git_server(self, production_ready=False, action="all"):
print(t("git_repo_deploy_starting"))
cmd = (
"python3 ./script/git/git_local_server.py -v" f" --action {action}"
)
if production_ready:
cmd += " --production-ready"
self.execute.exec_command_live(
cmd,
source_erplibre=False,
)
def prompt_execute_gpt_code(self):
print(f"🤖 {t('gpt_code_manage')}")
choices = [
{"prompt_description": t("gpt_code_claude_commit")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self._setup_claude_commit()
else:
print(t("cmd_not_found"))
def _setup_claude_commit(self):
dest_dir = os.path.expanduser("~/.claude/commands")
dest_file = os.path.join(dest_dir, "commit.md")
if os.path.exists(dest_file):
print(t("gpt_code_commit_exists"))
return
name = input(t("gpt_code_enter_name")).strip()
email = input(t("gpt_code_enter_email")).strip()
template_path = os.path.join(
os.path.dirname(__file__),
"..",
"..",
"conf",
"template_claude_commands_commit.md",
)
try:
with open(template_path) as f:
content = f.read()
content = content.replace(
"Your Name <your@email.com>",
f"{name} <{email}>",
)
content = content.replace(
"Your Name ",
f"{name} ",
)
os.makedirs(dest_dir, exist_ok=True)
with open(dest_file, "w") as f:
f.write(content)
print(t("gpt_code_commit_created"))
except Exception as e:
print(f"{t('gpt_code_commit_error')}{e}")
def prompt_execute_doc(self):
print(f"🤖 {t('doc_search')}")
choices = [
{"prompt_description": t("migration_module_coverage")},
{"prompt_description": t("what_change_between_version")},
{"prompt_description": t("oca_guidelines")},
{"prompt_description": t("oca_migration_odoo_19")},
]
help_info = self.fill_help_info(choices)
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(t("cmd_not_found"))
def prompt_execute_database(self):
print(f"🤖 {t('db_modify')}")
choices = [
{"prompt_description": t("download_db_backup")},
{"prompt_description": t("restore_from_backup")},
{"prompt_description": t("create_backup")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.db_manager.download_database_backup_cli()
elif status == "2":
self.db_manager.restore_from_database()
elif status == "3":
self.db_manager.create_backup_from_database()
else:
print(t("cmd_not_found"))
def prompt_execute_process(self):
print(f"🤖 {t('process_manage')}")
choices = [
{"prompt_description": t("kill_process_port")},
{"prompt_description": t("kill_git_daemon")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.process_kill_from_port()
elif status == "2":
self.process_kill_git_daemon()
else:
print(t("cmd_not_found"))
def process_kill_git_daemon(self):
self.execute.exec_command_live(
"pkill -f 'git daemon'",
source_erplibre=False,
)
print(t("kill_git_daemon_done"))
def prompt_execute_config(self):
print(f"🤖 {t('config_manage')}")
choices = [
{"prompt_description": t("generate_all_config")},
{"prompt_description": t("generate_from_preconfig")},
{"prompt_description": t("generate_from_backup")},
{"prompt_description": t("generate_from_database")},
{"prompt_description": t("setup_queue_job_for_parallelism")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.generate_config()
elif status == "2":
self.generate_config_from_preconfiguration()
elif status == "3":
self.generate_config_from_backup()
elif status == "4":
self.generate_config_from_database()
elif status == "5":
self.generate_config_queue_job()
else:
print(t("cmd_not_found"))
def prompt_execute_network(self):
print(f"🤖 {t('network_tools')}")
choices = [
{"prompt_description": t("ssh_port_forwarding")},
{
"prompt_description": t(
"network_performance_request_per_second"
)
},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.generate_network_port_forwarding()
elif status == "2":
self.generate_network_performance_test()
else:
print(t("cmd_not_found"))
def generate_network_port_forwarding(self, add_arg=None):
# ssh -L local_port:localhost:remote_port SSH_connection
ssh_connection = click.prompt(
"SSH connection, check ~/.ssh/config or user@address"
)
local_port = click.prompt("local port (8069)")
remote_port = click.prompt("remote port (8069)")
cmd = f"ssh -L {local_port}:localhost:{remote_port} {ssh_connection}"
self.execute.exec_command_live(
cmd,
source_erplibre=False,
single_source_erplibre=False,
)
def generate_network_performance_test(self, add_arg=None):
# ./script/performance/test_performance.sh
address = click.prompt("https address, like https://erplibre.com")
cmd = f"./script/performance/test_performance.sh {address}"
self.execute.exec_command_live(
cmd,
source_erplibre=False,
single_source_erplibre=True,
)
def prompt_execute_security(self):
print(f"🤖 {t('security_audit')}")
choices = [
{"prompt_description": t("pip_audit_desc")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.execute_pip_audit()
else:
print(t("cmd_not_found"))
def prompt_execute_test(self):
print(f"🤖 {t('test_description')}")
choices = [
{"prompt_description": t("test_run_module")},
{"prompt_description": t("test_run_module_coverage")},
{"prompt_description": t("test_run_unit_tests")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.execute_test_module(coverage=False)
elif status == "2":
self.execute_test_module(coverage=True)
elif status == "3":
self.execute_unit_tests()
else:
print(t("cmd_not_found"))
def execute_test_module(self, coverage=False):
# Module name
module_name = input(t("test_enter_module_name")).strip()
if not module_name:
print(t("test_module_required"))
return
# Database name
db_name = input(t("test_db_name")).strip()
if not db_name:
db_name = "test_todo_tmp"
# Extra modules
extra_modules = input(t("test_install_extra_modules")).strip()
# Log level
log_level = input(t("test_log_level")).strip()
if not log_level:
log_level = "test"
# Build module list
modules_to_install = module_name
if extra_modules:
modules_to_install += f",{extra_modules}"
# Step 1: Create temp DB
print(f"\n--- {t('test_creating_db')} '{db_name}' ---")
cmd_restore = f"./script/database/db_restore.py --database {db_name}"
self.execute.exec_command_live(
cmd_restore,
source_erplibre=False,
single_source_erplibre=True,
)
# Step 2: Install modules
print(
f"\n--- {t('test_installing_modules')}: {modules_to_install} ---"
)
cmd_install = (
f"./script/addons/install_addons.sh"
f" {db_name} {modules_to_install}"
)
self.execute.exec_command_live(
cmd_install,
source_erplibre=False,
single_source_erplibre=True,
)
# Step 3: Run tests
print(f"\n--- {t('test_running')}: {module_name} ---")
cmd_test = (
f"ODOO_MODE_TEST=true"
f" ./run.sh"
f" -d {db_name}"
f" -u {module_name}"
f" --log-level={log_level}"
)
if coverage:
cmd_test = f"ODOO_MODE_COVERAGE=true {cmd_test}"
status_code, output = self.execute.exec_command_live(
cmd_test,
return_status_and_output=True,
source_erplibre=False,
single_source_erplibre=True,
)
if status_code == 0:
print(f"\n{t('test_success')}")
else:
print(f"\n{t('test_failed')} {status_code}")
# Step 4: Cleanup
lang = get_lang()
keep_input = input(t("test_keep_db")).strip().lower()
keep = keep_input in (("o", "oui") if lang == "fr" else ("y", "yes"))
if keep:
print(f"{t('test_db_kept')}: {db_name}")
else:
print(f"\n--- {t('test_cleaning_db')} '{db_name}' ---")
cmd_drop = f"./odoo_bin.sh db --drop --database {db_name}"
self.execute.exec_command_live(
cmd_drop,
source_erplibre=False,
single_source_erplibre=True,
)
def execute_unit_tests(self):
print(f"\n--- {t('test_unit_running')} ---")
cmd = (
".venv.erplibre/bin/python -m unittest discover"
" -s test -p 'test_*.py' -v"
)
status_code, output = self.execute.exec_command_live(
cmd,
source_erplibre=False,
return_status_and_output=True,
)
if status_code == 0:
print(f"\n{t('test_unit_success')}")
else:
print(f"\n{t('test_unit_failed')}: {status_code}")
def execute_pip_audit(self):
versions, installed_versions, odoo_installed_version = (
get_odoo_version()
)
# Build list of installed environments
environments = {}
key_i = 0
for version_info in versions[::-1]:
erplibre_version = version_info.get("erplibre_version")
venv_path = f".venv.{erplibre_version}"
req_path = f"requirement/requirements.{erplibre_version}.txt"
odoo_version = f"odoo{version_info.get('odoo_version')}"
if not os.path.isdir(venv_path):
continue
key_i += 1
key_s = str(key_i)
label = f"{key_s}: {erplibre_version}"
if odoo_version == odoo_installed_version:
label += f" - {t('current')}"
if version_info.get("default"):
label += f" - {t('default')}"
environments[key_s] = {
"label": label,
"venv_path": venv_path,
"req_path": req_path,
"erplibre_version": erplibre_version,
}
if not environments:
print(t("no_env_installed"))
return
# Show selection menu
str_input = (
f"💬 {t('choose_env_audit')}\n\t"
+ "\n\t".join([v["label"] for v in environments.values()])
+ f"\n\t0: {t('back')}"
+ f"\n{t('selection')}"
)
env_input = ""
while env_input not in environments and env_input != "0":
if env_input:
print(f"{t('error_value')}" f" '{env_input}'")
env_input = input(str_input).strip()
if env_input == "0":
return
selected = environments[env_input]
venv_path = selected["venv_path"]
req_path = selected["req_path"]
if not os.path.isfile(req_path):
print(f"{t('dep_file_not_found')}{req_path}")
return
# TODO support bash from parameter if open gnome-terminal
cmd = f"pip-audit -r {req_path} -l;bash"
print(f"{t('execution')}{cmd}")
self.execute.exec_command_live(
cmd,
source_erplibre=True,
single_source_erplibre=False,
)
def generate_config(self, add_arg=None):
# Repeating to get all item before get group
cmd = (
f"./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA;"
f"./script/git/git_repo_update_group.py;"
f"./script/generate_config.sh"
)
if add_arg:
cmd += (
f";./script/git/git_repo_update_group.py {add_arg};"
f"./script/generate_config.sh"
)
self.execute.exec_command_live(
cmd,
source_erplibre=False,
single_source_erplibre=True,
)
def generate_config_from_preconfiguration(self):
choices = [
{"prompt_description": t("preconfig_base")},
{"prompt_description": t("preconfig_base_code_generator")},
{"prompt_description": t("preconfig_base_image_db")},
{"prompt_description": t("preconfig_all")},
# {"prompt_description": "base + migration"},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
group = "base"
str_group = f"--group {group}"
self.generate_config(add_arg=str_group)
elif status == "2":
group = "base,code_generator"
str_group = f"--group {group}"
self.generate_config(add_arg=str_group)
elif status == "3":
group = "base,image_db"
str_group = f"--group {group}"
self.generate_config(add_arg=str_group)
elif status == "4":
self.generate_config()
# elif status == "5":
# group = "base,migration"
# str_group = f"--group {group}"
# self.generate_config(add_arg=str_group)
else:
print(t("cmd_not_found"))
def debug_ide(self):
choices = [
{"prompt_description": t("debug_todo_py")},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.open_pycharm_file(
os.getcwd(),
os.path.join(os.getcwd(), "script/todo/todo.py"),
)
else:
print(t("cmd_not_found"))
def generate_config_from_backup(self):
file_name = self.db_manager.open_file_image_db()
add_arg = f"--from_backup_name {file_name} --add_repo odoo18.0/addons/MathBenTech_development"
self.generate_config(add_arg=add_arg)
def generate_config_from_database(self):
database_name = self.db_manager.select_database()
str_arg = f"--database {database_name}"
self.generate_config(add_arg=str_arg)
return False
def generate_config_queue_job(self):
cmd = "./script/config/setup_odoo_config_conf_devops.py"
self.execute.exec_command_live(
cmd,
source_erplibre=False,
single_source_erplibre=True,
)
def prompt_execute_selenium_and_run_db(
self, db_name, extra_cmd_web_login=""
):
cmd_server = f"./run.sh -d {db_name};bash"
self.execute.exec_command_live(cmd_server)
cmd_client = (
f"sleep 3;./script/selenium/web_login.py{extra_cmd_web_login};bash"
)
self.execute.exec_command_live(cmd_client)
def prompt_execute_selenium(self, command=None, extra_cmd_web_login=""):
commands = []
if not command:
cmd = "./script/selenium/web_login.py"
else:
cmd = command
if type(extra_cmd_web_login) is list:
for item in extra_cmd_web_login:
commands.append(cmd + item)
else:
commands.append(cmd + extra_cmd_web_login)
if len(commands) == 1:
self.execute.exec_command_live(commands[0])
elif len(commands) > 1:
new_cmd = "parallel ::: "
for i, cmd in enumerate(commands):
new_cmd += f' "sleep {1 * i};{cmd}"'
self.execute.exec_command_live(new_cmd)
def crash_diagnostic(self, e):
# TODO show message at start if os.path.exists(ERROR_LOG_PATH)
if os.path.exists(ERROR_LOG_PATH) and not os.path.exists(
VENV_ERPLIBRE
):
print("Got error : ")
print(e)
print("Got error at first execution.", ERROR_LOG_PATH)
try:
file = open(ERROR_LOG_PATH, "r")
content = file.read()
# TODO si vide, ajouter notre erreur
print(content)
except FileNotFoundError:
print("Error: File not found.")
finally:
if "file" in locals() and file:
file.close()
# Force auto installation
print("Auto installation")
time.sleep(0.5)
cmd = "./script/todo/source_todo.sh"
# self.restart_script(e)
self.execute.exec_command_live(cmd, source_erplibre=True)
sys.exit(1)
if os.path.exists(VENV_ERPLIBRE):
print("Import error : ")
print(e)
# TODO auto-detect gnome-terminal, or choose another. Is it done already?
self.restart_script(e)
# self.prompt_install()
# print(
# f"You forgot to activate source \nsource ./{VENV_ERPLIBRE}/bin/activate"
# )
# time.sleep(0.5)
# cmd = "./script/todo/source_todo.sh"
print("Re-execute TODO 🤖 or execute :")
print()
print(f"source {VENV_ERPLIBRE}/bin/activate;make")
print()
cmd = "./script/todo/todo.py"
# # self.restart_script(e)
try:
# TODO duplicate
import tkinter as tk
from tkinter import filedialog
import click
import humanize
import openai
import urwid
from pykeepass import PyKeePass
except ImportError:
print("Rerun and exit")
self.execute.exec_command_live(cmd, source_erplibre=True)
sys.exit(1)
print("No error")
else:
self.prompt_install()
def open_shell_on_database(self):
database = self.db_manager.select_database()
if database:
cmd_server = f"./odoo_bin.sh shell -d {database}"
status, databases = self.execute.exec_command_live(
cmd_server,
return_status_and_output=True,
source_erplibre=False,
single_source_erplibre=True,
new_window=True,
)
def open_pycharm_file(self, folder, filename):
cmd = "~/.local/share/JetBrains/Toolbox/scripts/pycharm"
# cmd = "/snap/bin/pycharm-community"
# if pycharm_arg:
# cmd += f" {pycharm_arg}"
if folder:
cmd += f" {folder}"
if filename:
cmd += f" --line 1 {filename}"
self.execute.exec_command_live(cmd, source_erplibre=False)
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.execute.exec_command_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.execute.exec_command_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.execute.exec_command_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 callback_execute_custom_database(self, config):
database_name = self.db_manager.select_database()
self.prompt_execute_selenium_and_run_db(database_name)
def process_kill_from_port(self):
cfg = configparser.ConfigParser()
cfg.read("./config.conf")
http_port = cfg.getint("options", "http_port")
status = self.execute.exec_command_live(
f"./script/process/kill_process_by_port.py {http_port} --kill-tree --nb_parent 2",
source_erplibre=False,
)
def restart_script(self, last_error):
print(f"🤖 {t('reboot_todo')}")
# os.execv(sys.executable, ['python'] + sys.argv)
# TODO mettre check que le répertoire est créé, s'il existe, auto-loop à corriger
if os.path.exists(VENV_ERPLIBRE) and not os.path.exists(
ERROR_LOG_PATH
):
# TODO mettre check import suivant ne vont pas planter
try:
with open(ERROR_LOG_PATH, "w") as f_file:
f_file.write(str(last_error))
pass # The file is created and closed here, no content is written
print(
f"Try to reopen process with before :\nsource ./{VENV_ERPLIBRE}/bin/activate && exec python "
+ " ".join(sys.argv)
)
os.execv(
"/bin/bash",
[
"/bin/bash",
"-c",
f"source ./{VENV_ERPLIBRE}/bin/activate && exec python "
+ " ".join(sys.argv),
],
)
except Exception as e:
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()
def callback_make_mobile_home(self, config):
# Read file
default_project_name = "ERPLibre"
default_package_name = "ca.erplibre.home"
# Read default information
if os.path.exists(STRINGS_FILE):
tree = ET.parse(STRINGS_FILE)
root = tree.getroot()
for elem in root.findall("string"):
if elem.get("name") == "app_name":
default_project_name = elem.text
if elem.get("name") == "package_name":
default_package_name = elem.text
default_project_url_name = "https://erplibre.ca"
# Read default information
dotenv_file = dotenv.find_dotenv(
filename=os.path.join(MOBILE_HOME_PATH, "src", ".env.production")
)
default_project_url_name = dotenv.get_key(
dotenv_file, "VITE_WEBSITE_URL"
)
default_project_note_subject = dotenv.get_key(
dotenv_file, "VITE_LABEL_NOTE"
)
default_debug = False
project_name = default_project_name
project_url_name = default_project_url_name
project_principal_subject = default_project_note_subject
package_name = default_package_name
do_debug = default_debug
do_change_picture_menu = False
do_personalize = input(
"Do you want to personalize the mobile application (Y) : "
)
if do_personalize.strip().lower() == "y":
project_name = (
input(
f'Your project name (Separate by space in title), default "{default_project_name}" : '
).strip()
or default_project_name
)
package_name = (
input(
f'Your package name (separate by . lower case, 3 works like DOMAIN.NAME.OBJECT), default "{default_package_name}" : '
).strip()
or default_package_name
)
project_url_name = (
input(
f'Your project url website, default "{default_project_url_name}" : '
).strip()
or default_project_url_name
)
project_principal_subject = (
input(
f'Your project subject, default "{default_project_note_subject}" : '
).strip()
or default_project_note_subject
)
do_debug = (
input("Compilation with debug information, default No (Y) : ")
.strip()
.lower()
== "y"
)
do_change_picture_menu = (
input(
"Want to change picture from menu, you need android-studio (Y) : "
)
.strip()
.lower()
== "y"
)
# Rename with script bash
cmd_client = f'cd {MOBILE_HOME_PATH} && npx cap init "{project_name}" "{package_name}" && ./rename_android.sh "{project_name}" "{package_name}" && npx cap sync android'
self.execute.exec_command_live(cmd_client, source_erplibre=False)
# dotenv_mobile = dotenv.dotenv_values(dotenv_file)
# dotenv_mobile["VITE_TITLE"] = project_name
# dotenv_mobile["VITE_WEBSITE_URL"] = project_url_name
dotenv.set_key(
dotenv_file, "VITE_TITLE", project_name, quote_mode="always"
)
dotenv.set_key(
dotenv_file,
"VITE_WEBSITE_URL",
project_url_name,
quote_mode="always",
)
dotenv.set_key(
dotenv_file,
"VITE_LABEL_NOTE",
project_principal_subject,
quote_mode="always",
)
dotenv.set_key(
dotenv_file,
"VITE_DEBUG_DEV",
"true" if do_debug else "false",
quote_mode="never",
)
if do_change_picture_menu:
status = self.execute.exec_command_live(
f"cd {MOBILE_HOME_PATH} && npx cap open android;bash",
source_erplibre=False,
new_window=True,
)
print(
"Guide for Android-Studio, wait loading is finish. Right-click to app/New/Image Asset and load your image."
)
input(
"Did you finish to update image with Android-Studio ? Press to continue ..."
)
cmd_client = "cp ./mobile/erplibre_home_mobile/android/app/src/main/ic_launcher-playstore.png ./mobile/erplibre_home_mobile/src/assets/company_logo.png"
self.execute.exec_command_live(cmd_client, source_erplibre=False)
cmd_client = "cp ./mobile/erplibre_home_mobile/android/app/src/main/ic_launcher-playstore.png ./mobile/erplibre_home_mobile/src/assets/imgs/logo.png"
self.execute.exec_command_live(cmd_client, source_erplibre=False)
status = self.execute.exec_command_live(
"./mobile/compile_and_run.sh", source_erplibre=False
)
if __name__ == "__main__":
start_time = time.time()
try:
todo = TODO()
if ENABLE_CRASH:
todo.crash_diagnostic(CRASH_E)
todo.run()
except KeyboardInterrupt:
print(t("keyboard_interrupt"))
finally:
end_time = time.time()
duration_sec = end_time - start_time
if humanize:
duration_delta = datetime.timedelta(seconds=duration_sec)
humain_time = humanize.precisedelta(duration_delta)
print(f"\n{t('execution_time')} {humain_time}\n")
else:
print(f"\n{t('execution_time')} {duration_sec:.2f} sec.\n")