Compare commits
4 commits
4dd57e4c50
...
db1e9fa24f
| Author | SHA1 | Date | |
|---|---|---|---|
| db1e9fa24f | |||
| 839ef08ea4 | |||
| a2b507c471 | |||
| b856548d64 |
4 changed files with 268 additions and 39 deletions
|
|
@ -213,20 +213,19 @@ class TODO:
|
||||||
|
|
||||||
── {t("Sources & documentation")} ──
|
── {t("Sources & documentation")} ──
|
||||||
[7] {t("Git - Git tools")}
|
[7] {t("Git - Git tools")}
|
||||||
[8] {t("Update - Update all developed staging source code")}
|
[8] {t("Doc - Documentation search")}
|
||||||
[9] {t("Doc - Documentation search")}
|
|
||||||
|
|
||||||
── {t("AI & automation")} ──
|
── {t("AI & automation")} ──
|
||||||
[10] {t("GPT code - AI assistant tools")}
|
[9] {t("GPT code - AI assistant tools")}
|
||||||
[11] {t("Automation - Demonstration of developed features")}
|
[10] {t("Automation - Demonstration of developed features")}
|
||||||
|
|
||||||
── {t("Deployment, network & security")} ──
|
── {t("Deployment, network & security")} ──
|
||||||
[12] {t("Deploy - Deploy ERPLibre locally")}
|
[11] {t("Deploy - Deploy ERPLibre locally")}
|
||||||
[13] {t("Network - Network tools")}
|
[12] {t("Network - Network tools")}
|
||||||
[14] {t("Security - Dependency security audit")}
|
[13] {t("Security - Dependency security audit")}
|
||||||
|
|
||||||
── {t("Preferences")} ──
|
── {t("Preferences")} ──
|
||||||
[15] {t("Language - Change language / Changer la langue")}
|
[14] {t("Language - Change language / Changer la langue")}
|
||||||
[0] {t("Back")}
|
[0] {t("Back")}
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -263,34 +262,30 @@ class TODO:
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
elif status == "8":
|
elif status == "8":
|
||||||
status = self.prompt_execute_update()
|
|
||||||
if status is not False:
|
|
||||||
return
|
|
||||||
elif status == "9":
|
|
||||||
status = self.prompt_execute_doc()
|
status = self.prompt_execute_doc()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
elif status == "10":
|
elif status == "9":
|
||||||
status = self.prompt_execute_gpt_code()
|
status = self.prompt_execute_gpt_code()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
elif status == "11":
|
elif status == "10":
|
||||||
status = self.prompt_execute_function()
|
status = self.prompt_execute_function()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
elif status == "12":
|
elif status == "11":
|
||||||
status = self.prompt_execute_deploy()
|
status = self.prompt_execute_deploy()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
elif status == "13":
|
elif status == "12":
|
||||||
status = self.prompt_execute_network()
|
status = self.prompt_execute_network()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
elif status == "14":
|
elif status == "13":
|
||||||
status = self.prompt_execute_security()
|
status = self.prompt_execute_security()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
elif status == "15":
|
elif status == "14":
|
||||||
status = self._change_language()
|
status = self._change_language()
|
||||||
if status is not False:
|
if status is not False:
|
||||||
return
|
return
|
||||||
|
|
@ -4181,6 +4176,16 @@ class TODO:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Déplacé depuis le menu Execute : mise à jour de tout le code source
|
||||||
|
# de dev en staging (sous-menu de mise à jour).
|
||||||
|
choices.append(
|
||||||
|
{
|
||||||
|
"prompt_description": t(
|
||||||
|
"Update - Update all developed staging source code"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
help_info = self.fill_help_info(choices)
|
help_info = self.fill_help_info(choices)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -4189,10 +4194,12 @@ class TODO:
|
||||||
if status == "0":
|
if status == "0":
|
||||||
return False
|
return False
|
||||||
elif status == str(len(choices)):
|
elif status == str(len(choices)):
|
||||||
self.debug_ide()
|
self.prompt_execute_update()
|
||||||
elif status == str(len(choices) - 1):
|
elif status == str(len(choices) - 1):
|
||||||
self.upgrade_module()
|
self.debug_ide()
|
||||||
elif status == str(len(choices) - 2):
|
elif status == str(len(choices) - 2):
|
||||||
|
self.upgrade_module()
|
||||||
|
elif status == str(len(choices) - 3):
|
||||||
self.open_shell_on_database()
|
self.open_shell_on_database()
|
||||||
else:
|
else:
|
||||||
cmd_no_found = True
|
cmd_no_found = True
|
||||||
|
|
|
||||||
|
|
@ -580,20 +580,20 @@ TRANSLATIONS = {
|
||||||
"en": "Open ERPLibre with TODO 🤖",
|
"en": "Open ERPLibre with TODO 🤖",
|
||||||
},
|
},
|
||||||
"Update all erplibre_base on database test": {
|
"Update all erplibre_base on database test": {
|
||||||
"fr": "Mise à jour de tous les erplibre_base sur la base de données test",
|
"fr": "🔄 Mise à jour de tous les erplibre_base sur la base de données test",
|
||||||
"en": "Update all erplibre_base on database test",
|
"en": "🔄 Update all erplibre_base on database test",
|
||||||
},
|
},
|
||||||
"Show code status": {
|
"Show code status": {
|
||||||
"fr": "Afficher le statut du code",
|
"fr": "🔍 Afficher le statut du code",
|
||||||
"en": "Show code status",
|
"en": "🔍 Show code status",
|
||||||
},
|
},
|
||||||
"Stash all code": {
|
"Stash all code": {
|
||||||
"fr": "Remiser tout le code",
|
"fr": "📦 Remiser tout le code",
|
||||||
"en": "Stash all code",
|
"en": "📦 Stash all code",
|
||||||
},
|
},
|
||||||
"Format modified code": {
|
"Format modified code": {
|
||||||
"fr": "Formater le code modifié",
|
"fr": "🎨 Formater le code modifié",
|
||||||
"en": "Format modified code",
|
"en": "🎨 Format modified code",
|
||||||
},
|
},
|
||||||
# todo.py hardcoded prompt_descriptions
|
# todo.py hardcoded prompt_descriptions
|
||||||
"Mobile - Compile and run software": {
|
"Mobile - Compile and run software": {
|
||||||
|
|
@ -601,24 +601,24 @@ TRANSLATIONS = {
|
||||||
"en": "Mobile - Compile and run software",
|
"en": "Mobile - Compile and run software",
|
||||||
},
|
},
|
||||||
"Upgrade Odoo - Migration Database": {
|
"Upgrade Odoo - Migration Database": {
|
||||||
"fr": "Mise à jour Odoo - Migration de base de données",
|
"fr": "🚚 Mise à jour Odoo - Migration de base de données",
|
||||||
"en": "Upgrade Odoo - Migration Database",
|
"en": "🚚 Upgrade Odoo - Migration Database",
|
||||||
},
|
},
|
||||||
"Upgrade Poetry - Dependency of Odoo": {
|
"Upgrade Poetry - Dependency of Odoo": {
|
||||||
"fr": "Mise à jour Poetry - Dépendances d'Odoo",
|
"fr": "📦 Mise à jour Poetry - Dépendances d'Odoo",
|
||||||
"en": "Upgrade Poetry - Dependency of Odoo",
|
"en": "📦 Upgrade Poetry - Dependency of Odoo",
|
||||||
},
|
},
|
||||||
"Open SHELL": {
|
"Open SHELL": {
|
||||||
"fr": "Ouvrir le SHELL",
|
"fr": "🐚 Ouvrir le SHELL",
|
||||||
"en": "Open SHELL",
|
"en": "🐚 Open SHELL",
|
||||||
},
|
},
|
||||||
"Upgrade Module": {
|
"Upgrade Module": {
|
||||||
"fr": "Mise à jour de module",
|
"fr": "🧩 Mise à jour de module",
|
||||||
"en": "Upgrade Module",
|
"en": "🧩 Upgrade Module",
|
||||||
},
|
},
|
||||||
"Debug": {
|
"Debug": {
|
||||||
"fr": "Débogage",
|
"fr": "🐛 Débogage",
|
||||||
"en": "Debug",
|
"en": "🐛 Debug",
|
||||||
},
|
},
|
||||||
"Migration module coverage": {
|
"Migration module coverage": {
|
||||||
"fr": "Couverture de migration des modules",
|
"fr": "Couverture de migration des modules",
|
||||||
|
|
|
||||||
|
|
@ -210,12 +210,192 @@ def _menu_labels(cls) -> dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _config_list(config_key, todo_dir):
|
||||||
|
"""Entrées d'une liste de config de todo.json (ex. « code_from_makefile »),
|
||||||
|
cherchée à n'importe quel niveau. [] si absente."""
|
||||||
|
try:
|
||||||
|
data = json.loads(
|
||||||
|
(Path(todo_dir) / "todo.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return []
|
||||||
|
found = []
|
||||||
|
|
||||||
|
def walk(o):
|
||||||
|
if isinstance(o, dict):
|
||||||
|
for k, v in o.items():
|
||||||
|
if k == config_key and isinstance(v, list):
|
||||||
|
found.extend(v)
|
||||||
|
walk(v)
|
||||||
|
elif isinstance(o, list):
|
||||||
|
for x in o:
|
||||||
|
walk(x)
|
||||||
|
|
||||||
|
walk(data)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def _len_choices_offset(comp):
|
||||||
|
"""K pour « str(len(choices)) » -> 0 et « str(len(choices) - N) » -> N ;
|
||||||
|
None si le nœud n'est pas de cette forme."""
|
||||||
|
if not (
|
||||||
|
isinstance(comp, ast.Call)
|
||||||
|
and isinstance(comp.func, ast.Name)
|
||||||
|
and comp.func.id == "str"
|
||||||
|
and comp.args
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
arg = comp.args[0]
|
||||||
|
|
||||||
|
def is_len_choices(x):
|
||||||
|
return (
|
||||||
|
isinstance(x, ast.Call)
|
||||||
|
and isinstance(x.func, ast.Name)
|
||||||
|
and x.func.id == "len"
|
||||||
|
and x.args
|
||||||
|
and isinstance(x.args[0], ast.Name)
|
||||||
|
and x.args[0].id == "choices"
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_len_choices(arg):
|
||||||
|
return 0
|
||||||
|
if (
|
||||||
|
isinstance(arg, ast.BinOp)
|
||||||
|
and isinstance(arg.op, ast.Sub)
|
||||||
|
and is_len_choices(arg.left)
|
||||||
|
and isinstance(arg.right, ast.Constant)
|
||||||
|
):
|
||||||
|
return int(arg.right.value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch_len(func) -> dict:
|
||||||
|
"""{K: méthode} pour « status == str(len(choices) - K): self.M() »."""
|
||||||
|
out = {}
|
||||||
|
for node in ast.walk(func):
|
||||||
|
if not isinstance(node, ast.If):
|
||||||
|
continue
|
||||||
|
test = node.test
|
||||||
|
if not (
|
||||||
|
isinstance(test, ast.Compare)
|
||||||
|
and isinstance(test.left, ast.Name)
|
||||||
|
and test.left.id == "status"
|
||||||
|
and len(test.ops) == 1
|
||||||
|
and isinstance(test.ops[0], ast.Eq)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
k = _len_choices_offset(test.comparators[0])
|
||||||
|
if k is None:
|
||||||
|
continue
|
||||||
|
method = None
|
||||||
|
for stmt in node.body:
|
||||||
|
for n in ast.walk(stmt):
|
||||||
|
if (
|
||||||
|
isinstance(n, ast.Call)
|
||||||
|
and isinstance(n.func, ast.Attribute)
|
||||||
|
and isinstance(n.func.value, ast.Name)
|
||||||
|
and n.func.value.id == "self"
|
||||||
|
):
|
||||||
|
method = n.func.attr
|
||||||
|
break
|
||||||
|
if method:
|
||||||
|
break
|
||||||
|
if method:
|
||||||
|
out[k] = method
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _choices_children(func, todo_dir):
|
||||||
|
"""Commandes d'un menu bâti par « choices » (config_file.get_config +
|
||||||
|
choices.append/extend) avec dispatch « str(len(choices)-N) ». Renvoie une
|
||||||
|
liste de (label, méthode, kwargs) dans l'ordre affiché, ou None si le motif
|
||||||
|
ne s'applique pas. Les entrées de CONFIG rejouent via
|
||||||
|
execute_from_configuration ; les entrées APPENDÉES via leur méthode."""
|
||||||
|
def _dict_label(dnode):
|
||||||
|
d = {}
|
||||||
|
for k, v in zip(dnode.keys, dnode.values):
|
||||||
|
if isinstance(k, ast.Constant):
|
||||||
|
d[k.value] = _str_of(v)
|
||||||
|
return d.get("prompt_description") or d.get("prompt_description_key")
|
||||||
|
|
||||||
|
# On collecte affectations et append AVEC leur n° de ligne pour rejouer
|
||||||
|
# dans l'ORDRE (les entrées « menu_entry = {…}; choices.append(menu_entry) »
|
||||||
|
# réutilisent la même variable -> il faut suivre la dernière valeur).
|
||||||
|
config_key = None
|
||||||
|
events = [] # (lineno, kind, payload)
|
||||||
|
for node in ast.walk(func):
|
||||||
|
if (
|
||||||
|
isinstance(node, ast.Assign)
|
||||||
|
and len(node.targets) == 1
|
||||||
|
and isinstance(node.targets[0], ast.Name)
|
||||||
|
):
|
||||||
|
var = node.targets[0].id
|
||||||
|
val = node.value
|
||||||
|
if isinstance(val, ast.Dict):
|
||||||
|
events.append((node.lineno, "assign", (var, val)))
|
||||||
|
elif (
|
||||||
|
var == "choices"
|
||||||
|
and isinstance(val, ast.Call)
|
||||||
|
and isinstance(val.func, ast.Attribute)
|
||||||
|
and val.func.attr == "get_config"
|
||||||
|
and val.args
|
||||||
|
and isinstance(val.args[0], ast.Constant)
|
||||||
|
):
|
||||||
|
config_key = val.args[0].value
|
||||||
|
elif (
|
||||||
|
isinstance(node, ast.Call)
|
||||||
|
and isinstance(node.func, ast.Attribute)
|
||||||
|
and node.func.attr == "append"
|
||||||
|
and isinstance(node.func.value, ast.Name)
|
||||||
|
and node.func.value.id == "choices"
|
||||||
|
and node.args
|
||||||
|
):
|
||||||
|
events.append((node.lineno, "append", node.args[0]))
|
||||||
|
events.sort(key=lambda e: e[0])
|
||||||
|
var_dict, appended = {}, []
|
||||||
|
for _ln, kind, payload in events:
|
||||||
|
if kind == "assign":
|
||||||
|
var_dict[payload[0]] = payload[1]
|
||||||
|
else: # append : arg = Dict littéral ou Name (variable)
|
||||||
|
arg = payload
|
||||||
|
dnode = (
|
||||||
|
arg
|
||||||
|
if isinstance(arg, ast.Dict)
|
||||||
|
else var_dict.get(arg.id)
|
||||||
|
if isinstance(arg, ast.Name)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if dnode is not None:
|
||||||
|
lab = _dict_label(dnode)
|
||||||
|
if lab:
|
||||||
|
appended.append(lab)
|
||||||
|
if config_key is None and not appended:
|
||||||
|
return None
|
||||||
|
children = []
|
||||||
|
for entry in _config_list(config_key, todo_dir) if config_key else []:
|
||||||
|
lab = (
|
||||||
|
entry.get("prompt_description_key")
|
||||||
|
or entry.get("prompt_description")
|
||||||
|
or "?"
|
||||||
|
)
|
||||||
|
children.append((lab, "execute_from_configuration", {"instance": entry}))
|
||||||
|
len_disp = _dispatch_len(func)
|
||||||
|
n_config = len(children) # figé : `children` grossit dans la boucle
|
||||||
|
n_total = n_config + len(appended)
|
||||||
|
for j, lab in enumerate(appended):
|
||||||
|
pos = n_config + j # 0-based dans la liste globale (stable)
|
||||||
|
method = len_disp.get(n_total - 1 - pos) # None si non mappé
|
||||||
|
children.append((lab, method, {}))
|
||||||
|
return children
|
||||||
|
|
||||||
|
|
||||||
def build_code_tree(todo_path=None) -> dict | None:
|
def build_code_tree(todo_path=None) -> dict | None:
|
||||||
"""Construit l'arbre des menus/commandes EN LISANT le code de todo.py
|
"""Construit l'arbre des menus/commandes EN LISANT le code de todo.py
|
||||||
(AST). Chaque menu (méthode de _MENU_LABELS) devient un nœud ; ses branches
|
(AST). Chaque menu (méthode de _MENU_LABELS) devient un nœud ; ses branches
|
||||||
« status == N » deviennent des sous-menus (si la cible est un menu) ou des
|
« status == N » deviennent des sous-menus (si la cible est un menu) ou des
|
||||||
commandes (feuilles). None si l'analyse échoue."""
|
commandes (feuilles). None si l'analyse échoue."""
|
||||||
p = Path(todo_path) if todo_path else (Path(__file__).parent / "todo.py")
|
p = Path(todo_path) if todo_path else (Path(__file__).parent / "todo.py")
|
||||||
|
todo_dir = p.parent # todo.json est à côté de todo.py
|
||||||
try:
|
try:
|
||||||
mod = ast.parse(p.read_text(encoding="utf-8"))
|
mod = ast.parse(p.read_text(encoding="utf-8"))
|
||||||
except (OSError, SyntaxError):
|
except (OSError, SyntaxError):
|
||||||
|
|
@ -270,6 +450,25 @@ def build_code_tree(todo_path=None) -> dict | None:
|
||||||
"section": entry["section"] if entry else None,
|
"section": entry["section"] if entry else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# Menus bâtis par « choices » (get_config + append) sans dispatch
|
||||||
|
# littéral « status == "N" » : Code, Update, Git, Database…
|
||||||
|
if not disp:
|
||||||
|
for lab, tmethod, tkwargs in (
|
||||||
|
_choices_children(func, todo_dir) or []
|
||||||
|
):
|
||||||
|
if tmethod in labels: # une entrée qui ouvre un sous-menu
|
||||||
|
node["children"].append(build(tmethod))
|
||||||
|
else:
|
||||||
|
node["children"].append(
|
||||||
|
{
|
||||||
|
"label": lab,
|
||||||
|
"is_menu": False,
|
||||||
|
"children": [],
|
||||||
|
"method": tmethod,
|
||||||
|
"kwargs": tkwargs,
|
||||||
|
"section": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
seen.discard(method)
|
seen.discard(method)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,12 @@ import shutil
|
||||||
import sys
|
import sys
|
||||||
import zipfile
|
import zipfile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from script.todo.version_manager import get_odoo_version
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import todo_file_browser
|
import todo_file_browser
|
||||||
|
|
||||||
|
from script.todo.version_manager import get_odoo_version
|
||||||
|
|
||||||
new_path = os.path.normpath(
|
new_path = os.path.normpath(
|
||||||
os.path.join(os.path.dirname(__file__), "..", "..")
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
||||||
)
|
)
|
||||||
|
|
@ -768,6 +769,28 @@ class TodoUpgrade:
|
||||||
|
|
||||||
print("✅ -> Restore database")
|
print("✅ -> Restore database")
|
||||||
|
|
||||||
|
if not self.dct_progression.get("state_1_update_all"):
|
||||||
|
print(
|
||||||
|
"[1] Update all addons before neutralize (already neutralize by odoo if supported)"
|
||||||
|
)
|
||||||
|
wait_continue = (
|
||||||
|
input(
|
||||||
|
"💬 Do you need to upgrade before a database neutralization, press to ignore : "
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
if wait_continue == "1":
|
||||||
|
status, cmd_executed = self.todo_upgrade_execute(
|
||||||
|
f"./script/addons/update_addons_all.sh {database_name}",
|
||||||
|
single_source_odoo=True,
|
||||||
|
)
|
||||||
|
if not status:
|
||||||
|
self.dct_progression["state_1_update_all"] = True
|
||||||
|
self.write_config()
|
||||||
|
|
||||||
|
print("✅ -> Update database before neutralize by module")
|
||||||
|
|
||||||
print("✅ -> Neutralize database")
|
print("✅ -> Neutralize database")
|
||||||
if do_neutralize:
|
if do_neutralize:
|
||||||
status, cmd_executed = self.todo_upgrade_execute(
|
status, cmd_executed = self.todo_upgrade_execute(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue