erplibre/script/todo/todo.py

1314 lines
47 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
file_error_path = ".erplibre.error.txt"
cst_venv_erplibre = ".venv.erplibre"
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 = ".odoo-version"
ENABLE_CRASH = False
CRASH_E = None
# Support mobile ERPLibre
ANDROID_DIR = "android"
MOBILE_HOME_PATH = "./mobile/technolibre_home_mobile/technolibre_home"
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("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.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 \"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 = (
f"source ./{cst_venv_erplibre}/bin/activate;%s"
)
def run(self):
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!")
help_info = """Commande :
[1] Execute
[2] Install
[3] Question
[4] Fork - Ouvre TODO 🤖 dans une nouvelle tabulation
[0] Quitter
"""
while True:
try:
status = click.prompt(help_info)
except NameError:
print("Do")
print(f"source ./{cst_venv_erplibre}/bin/activate && make")
sys.exit(1)
except ImportError:
print("Do")
print(f"source ./{cst_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" ./{cst_venv_erplibre}/bin/activate;make todo'"
# )
cmd = "make todo"
self.executer_commande_live(cmd, source_erplibre=True)
# elif status == "3" or status == "install":
# print("install")
else:
print("Commande non trouvée 🤖!")
print(status)
# manipuler()
def get_kdbx(self):
if self.kdbx:
return self.kdbx
# Open file
chemin_fichier_kdbx = self.config_file.get_config_value(
["kdbx", "path"]
)
if not chemin_fichier_kdbx:
root = tk.Tk()
root.withdraw() # Hide the main window
chemin_fichier_kdbx = filedialog.askopenfilename(
title="Select a File",
filetypes=(("KeepassX files", "*.kdbx"),),
)
if not chemin_fichier_kdbx:
_logger.error(
f"KDBX is not configured, please fill {self.config_file.CONFIG_FILE}"
)
return
mot_de_passe_kdbx = self.config_file.get_config_value(
["kdbx", "password"]
)
if not mot_de_passe_kdbx:
mot_de_passe_kdbx = getpass.getpass(
prompt="Entrez votre mot de passe : "
)
kp = PyKeePass(chemin_fichier_kdbx, password=mot_de_passe_kdbx)
if kp:
self.kdbx = kp
return kp
def execute_prompt_ia(self):
while True:
help_info = """Commande :
[0] Retour
Écrit moi ta question """
status = click.prompt(help_info)
print()
if status == "0":
return
kp = self.get_kdbx()
if not kp:
return
nom_configuration = self.config_file.get_config_value(
["kdbx_config", "openai", "kdbx_key"]
)
entry = kp.find_entries_by_title(nom_configuration, 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 = """Commande :
[1] Run - Exécuter et installer une instance
[2] 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
[7] Process - Outils sur les exécutions
[0] Retour
"""
while True:
status = click.prompt(help_info)
print()
if status == "0":
return
elif status == "1":
status = self.prompt_execute_instance()
if status is not False:
return
elif status == "2":
status = self.prompt_execute_fonction()
if status is not False:
return
elif status == "3":
status = self.prompt_execute_update()
if status is not False:
return
elif status == "4":
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
elif status == "7":
status = self.prompt_execute_process()
if status is not False:
return
else:
print("Commande non trouvée 🤖!")
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.executer_commande_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.executer_commande_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
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",
),
"m": (
"m",
"m: ERPLibre with mobile home",
"./mobile/install_and_run.sh",
),
"0": (
"0",
"0: Quitter",
),
}
dct_final_cmd_intern = {}
lst_version, lst_version_installed, odoo_installed_version = (
self.get_odoo_version()
)
for dct_version in lst_version[::-1]:
key_i += 1
key_s = str(key_i)
label = f"{key_s}: Odoo {dct_version.get('odoo_version')}"
odoo_version = f"odoo{dct_version.get('odoo_version')}"
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}
# 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}")
# 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
):
# exec_run_db need argument database
kdbx_key = dct_instance.get("kdbx_key")
odoo_user = dct_instance.get("user")
odoo_password = dct_instance.get("password")
if kdbx_key:
extra_cmd_web_login = self.kdbx_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 = dct_instance.get("makefile_cmd")
if makefile_cmd and not ignore_makefile:
status = self.executer_commande_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 = dct_instance.get("database")
self.prompt_execute_selenium_and_run_db(
db_name, extra_cmd_web_login=extra_cmd_web_login
)
command = dct_instance.get("command")
if command:
self.prompt_execute_selenium(
command=command, extra_cmd_web_login=extra_cmd_web_login
)
callback = dct_instance.get("callback")
if callback:
callback(dct_instance)
def fill_help_info(self, lst_instance):
help_info = "Commande :\n"
help_end = "[0] Retour\n"
for i, dct_instance in enumerate(lst_instance):
help_info += (
f"[{i + 1}] " + dct_instance["prompt_description"] + "\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
lst_instance = self.config_file.get_config("instance")
init_len = len(lst_instance)
if os.path.exists(MOBILE_HOME_PATH):
dct_upgrade_odoo_database = {
"prompt_description": "Mobile - Compile and run software",
"callback": self.callback_make_mobile_home,
}
lst_instance.append(dct_upgrade_odoo_database)
help_info = self.fill_help_info(lst_instance)
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 <= init_len:
cmd_no_found = False
status = click.confirm(
"Voulez-vous une nouvelle instance?"
)
dct_instance = lst_instance[int_cmd - 1]
self.execute_from_configuration(
dct_instance,
exec_run_db=True,
ignore_makefile=not bool(status),
)
elif int_cmd <= len(lst_instance):
# Execute dynamic instance
dct_instance = lst_instance[int_cmd - 1]
self.execute_from_configuration(
dct_instance,
)
except ValueError:
pass
if cmd_no_found:
print("Commande non trouvée 🤖!")
def prompt_execute_fonction(self):
lst_instance = self.config_file.get_config("function")
help_info = self.fill_help_info(lst_instance)
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(lst_instance):
cmd_no_found = False
dct_instance = lst_instance[int_cmd - 1]
self.execute_from_configuration(dct_instance)
except ValueError:
pass
if cmd_no_found:
print("Commande non trouvée 🤖!")
def prompt_execute_update(self):
# self.executer_commande_live(f"make {makefile_cmd}")
print("🤖 Mise à jour du développement")
# 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
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:
status = click.prompt(help_info)
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) - 1
if 0 < int_cmd <= len(lst_instance):
cmd_no_found = False
dct_instance = lst_instance[int_cmd - 1]
self.execute_from_configuration(dct_instance)
except ValueError:
pass
if cmd_no_found:
print("Commande non trouvée 🤖!")
def prompt_execute_code(self):
print("🤖 Qu'avez-vous de besoin pour développer?")
# 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
# """
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:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == str(len(lst_instance)):
self.upgrade_module()
else:
cmd_no_found = True
try:
int_cmd = int(status)
if 0 < int_cmd <= len(lst_instance):
cmd_no_found = False
dct_instance = lst_instance[int_cmd - 1]
self.execute_from_configuration(dct_instance)
except ValueError:
pass
if cmd_no_found:
print("Commande non trouvée 🤖!")
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 prompt_execute_process(self):
print("🤖 Manipuler les processus d'exécution!")
lst_instance = [
{"prompt_description": "Kill process from actual port"},
]
help_info = self.fill_help_info(lst_instance)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self.process_kill_from_port()
else:
print("Commande non trouvée 🤖!")
def get_odoo_version(self):
with open(VERSION_DATA_FILE) as txt:
data_version = json.load(txt)
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 = []
if kdbx_key:
kp = self.get_kdbx()
if not kp:
return ""
if type(kdbx_key) is not list:
lst_kdbx_key = [kdbx_key]
else:
lst_kdbx_key = kdbx_key
for key in lst_kdbx_key:
entry = kp.find_entries_by_title(key, first=True)
try:
odoo_user = entry.username
except AttributeError:
_logger.error(f"Cannot find username from keys {key}")
try:
odoo_password = entry.password
except AttributeError:
_logger.error(f"Cannot find password from keys {key}")
lst_value.append(
" --default_email_auth"
f" {odoo_user} --default_password_auth '{odoo_password}'"
)
if len(lst_value) == 0:
return ""
elif len(lst_value) == 1:
return lst_value[0]
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}"'
# )
# 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_client)
def prompt_execute_selenium(self, command=None, extra_cmd_web_login=""):
lst_cmd = []
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:
lst_cmd.append(cmd + item)
else:
lst_cmd.append(cmd + extra_cmd_web_login)
if len(lst_cmd) == 1:
self.executer_commande_live(lst_cmd[0])
elif len(lst_cmd) > 1:
new_cmd = "parallel ::: "
for i, cmd in enumerate(lst_cmd):
new_cmd += f' "sleep {1 * i};{cmd}"'
self.executer_commande_live(new_cmd)
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.
Args:
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 = (
# f"gnome-terminal --tab -- bash -c 'source"
# f" ./{cst_venv_erplibre}/bin/activate;{commande}'"
# )
commande = self.cmd_source_erplibre % 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()
if not source_odoo:
_logger.error(
f"You cannot execute Odoo command if no version is installed. Command : {commande}"
)
return -1
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,
shell=True,
executable="/bin/bash",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
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:
ligne = process.stdout.readline()
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
return_status = process.returncode
if process.returncode != 0 and not quiet:
print(
"La commande a retourné un code d'erreur :"
f" {process.returncode}"
)
except FileNotFoundError:
if "password" in commande:
print(
f"Erreur : La commande '{commande.split(' ')[0]}'[...] n'a"
" pas été trouvée."
)
else:
print(
f"Erreur : La commande '{commande}' n'a pas été trouvée."
)
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)
if os.path.exists(file_error_path) and not os.path.exists(
cst_venv_erplibre
):
print("Got error : ")
print(e)
print("Got error at first execution.", file_error_path)
try:
file = open(file_error_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.executer_commande_live(cmd, source_erplibre=True)
sys.exit(1)
if os.path.exists(cst_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 ./{cst_venv_erplibre}/bin/activate"
# )
# time.sleep(0.5)
# cmd = "./script/todo/source_todo.sh"
print("Re-execute TODO 🤖 or execute :")
print()
print(f"source {cst_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.executer_commande_live(cmd, source_erplibre=True)
sys.exit(1)
print("No error")
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 process_kill_from_port(self):
cfg = configparser.ConfigParser()
cfg.read("./config.conf")
http_port = cfg.getint("options", "http_port")
status = self.executer_commande_live(
f"./script/process/kill_process_by_port.py {http_port} --kill-tree --nb_parent 2",
source_erplibre=False,
)
def download_database_backup_cli(self, show_remote_list=True):
database_domain = input("Domain Odoo (ex. https://mondomain.com) : ")
if show_remote_list:
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)
# TODO mettre check que le répertoire est créé, s'il existe, auto-loop à corriger
if os.path.exists(cst_venv_erplibre) and not os.path.exists(
file_error_path
):
# TODO mettre check import suivant ne vont pas planter
try:
with open(file_error_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 ./{cst_venv_erplibre}/bin/activate && exec python "
+ " ".join(sys.argv)
)
os.execv(
"/bin/bash",
[
"/bin/bash",
"-c",
f"source ./{cst_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, dct_config):
# Read file
default_project_name = "ERPLibre"
default_package_name = "ca.erplibre.home"
# Read default information
if os.path.exists(STRINGS_FILE):
tree = ET.parse(STRINGS_FILE)
root = tree.getroot()
for elem in root.findall("string"):
if elem.get("name") == "app_name":
default_project_name = elem.text
if elem.get("name") == "package_name":
default_package_name = elem.text
default_project_url_name = "https://erplibre.ca"
# Read default information
dotenv_file = dotenv.find_dotenv(
filename=os.path.join(MOBILE_HOME_PATH, "src", ".env.production")
)
default_project_url_name = dotenv.get_key(
dotenv_file, "VITE_WEBSITE_URL"
)
default_debug = False
project_name = default_project_name
project_url_name = default_project_url_name
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
)
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.executer_commande_live(cmd_client, source_erplibre=False)
# dotenv_mobile = dotenv.dotenv_values(dotenv_file)
# dotenv_mobile["VITE_TITLE"] = project_name
# dotenv_mobile["VITE_WEBSITE_URL"] = project_url_name
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_DEBUG_DEV",
"true" if do_debug else "false",
quote_mode="never",
)
if do_change_picture_menu:
status = self.executer_commande_live(
f"cd {MOBILE_HOME_PATH} && npx cap open android;bash",
source_erplibre=False,
new_window=True,
)
print(
"Guide for Android-Studio, wait loading is finish. Right-click to app/New/Image Asset and load your image."
)
input(
"Did you finish to update image with Android-Studio ? Press to continue ..."
)
status = self.executer_commande_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("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"\nTODO execution time {humain_time}\n")
else:
print(f"\nTODO execution time {duration_sec:.2f} sec.\n")