diff --git a/.claude/rules/01-versions.md b/.claude/rules/01-versions.md new file mode 100644 index 0000000..5201eed --- /dev/null +++ b/.claude/rules/01-versions.md @@ -0,0 +1,14 @@ +# Versions supportées + +| Odoo | Python | Poetry | Statut | +|-------|----------|--------|------------| +| 18.0 | 3.12.10 | 2.1.3 | **Défaut** | +| 17.0 | 3.10.18 | 1.8.3 | Actif | +| 16.0 | 3.10.18 | 1.8.3 | Actif | +| 15.0 | 3.8.20 | 1.8.3 | Déprécié | +| 14.0 | 3.8.20 | 1.5.0 | Déprécié | +| 13.0 | 3.7.17 | 1.5.0 | Déprécié | +| 12.0 | 3.7.17 | 1.5.0 | Déprécié | + +Configuration dans `conf/supported_version_erplibre.json`. +Fichiers de version : `.odoo-version`, `.erplibre-version`, `.poetry-version`, `.python-odoo-version`. diff --git a/.claude/rules/02-project-structure.md b/.claude/rules/02-project-structure.md new file mode 100644 index 0000000..dffc4c0 --- /dev/null +++ b/.claude/rules/02-project-structure.md @@ -0,0 +1,45 @@ +# Structure du projet + +``` +erplibre/ +├── Makefile # Orchestrateur principal (inclut conf/make.*.Makefile) +├── run.sh / odoo_bin.sh # Lanceurs Odoo (venv + PYTHONPATH) +├── env_var.sh # Variables d'environnement globales +├── conf/ # Configuration : Makefiles modulaires, versions, manifests CSV +│ ├── make.installation.Makefile +│ ├── make.test.Makefile +│ ├── make.database.Makefile +│ ├── make.docker.Makefile +│ ├── make.code_generator.Makefile +│ ├── make.installation.poetry.Makefile +│ └── supported_version_erplibre.json +├── manifest/ # Manifests Google Repo (XML) par version Odoo +│ └── git_manifest_odoo{12..18}.0.xml +├── requirement/ # Dépendances par version +│ ├── pyproject.odooXX.0_pythonY.Z.toml +│ ├── poetry.odooXX.0_pythonY.Z.lock +│ └── requirements.odooXX.0_pythonY.Z.txt +├── script/ # Scripts utilitaires (32+ catégories) +│ ├── todo/ # CLI interactif principal (todo.py) +│ ├── database/ # Opérations DB (restore, migrate, image_db) +│ ├── addons/ # Gestion des modules (install, update, uninstall) +│ ├── code_generator/ # Génération de modules Odoo +│ ├── version/ # Changement de version +│ ├── git/ # Opérations Git et Google Repo +│ ├── maintenance/ # Formatage (black, isort, prettier) +│ ├── test/ # Tests parallèles + coverage +│ ├── docker/ # Build/run Docker +│ ├── poetry/ # Gestion Poetry +│ ├── deployment/ # Déploiement production +│ └── selenium/ # Tests web automatisés +├── docker/ # Dockerfiles + docker-compose par version +├── addons/ # Répertoire des addons (géré par Google Repo) +│ ├── OCA_*/ # Modules OCA +│ ├── ERPLibre_*/ # Modules ERPLibre +│ ├── TechnoLibre_*/ # Code generator + templates +│ └── MathBenTech_*/ # Modules spécialisés +├── odoo{12..18}.0/ # Sources Odoo par version +├── doc/ # Documentation (DEVELOPMENT, PRODUCTION, MIGRATION, etc.) +├── test/ # Framework de test +└── private/ # Fichiers privés (non versionné) +``` diff --git a/.claude/rules/03-commands.md b/.claude/rules/03-commands.md new file mode 100644 index 0000000..06dd2ae --- /dev/null +++ b/.claude/rules/03-commands.md @@ -0,0 +1,63 @@ +# Commandes essentielles + +## Changement de version Odoo +```bash +make switch_odoo_18 # Passer à Odoo 18 +make switch_odoo_17 # Passer à Odoo 17 +make switch_odoo_16 # Passer à Odoo 16 (défaut) +make switch_odoo_15 # Passer à Odoo 15 +make switch_odoo_14 # Passer à Odoo 14 +make switch_odoo_13 # Passer à Odoo 13 +make switch_odoo_12 # Passer à Odoo 12 +make switch_odoo_16_update # Passer + mettre à jour les dépendances +``` + +## Exécution +```bash +make run # Lancer Odoo (port 8069) +make run_test # Lancer avec la DB "test" +./run.sh -d ma_base # Lancer avec une DB spécifique +./odoo_bin.sh [args] # Exécuter odoo-bin directement +``` + +## Base de données +```bash +make db_create_db_test # Créer la DB "test" +make db_clone_test_to_test2 # Cloner test -> test2 +./script/database/db_restore.py --database NOM --image IMAGE +./script/addons/install_addons_dev.sh DB module1,module2 +./script/addons/update_addons_all.sh DB +``` + +## Tests +```bash +make test # Tests de base + format +make test_full_fast # Tests complets en parallèle +make test_full_fast_coverage # Avec couverture de code +``` + +## Formatage du code +```bash +make format # Fichiers à commiter uniquement +make format_all # Tout formater (parallèle) +``` + +## Docker +```bash +make docker_run_daemon # Lancer en production +make docker_build_odoo_18 # Builder l'image Odoo 18 +``` + +## Google Repo (gestion multi-dépôts) +```bash +make repo_show_status # Statut de tous les dépôts addons +make repo_configure_all # Configurer tous les dépôts +make repo_do_stash # Stash tous les dépôts +.venv.erplibre/bin/repo sync # Synchroniser les dépôts +``` + +## Installation +```bash +make install_os # Dépendances système +make install_odoo_18 # Installer Odoo 18 complet +``` diff --git a/.claude/rules/04-code-conventions.md b/.claude/rules/04-code-conventions.md new file mode 100644 index 0000000..7238f5d --- /dev/null +++ b/.claude/rules/04-code-conventions.md @@ -0,0 +1,24 @@ +# Conventions de code + +## Python +- Formateur : **Black** (profil par défaut, ligne max 79 pour les modules Odoo) +- Imports : **isort** avec profil `black`, longueur de ligne 79 +- Linting : **Flake8** avec bugbear, max-line-length 80, max-complexity 16 +- Ignorer : E203, E501, W503 (compatibilité Black) + +## XML / JSON / YAML +- Formateur : **Prettier** (via npm) +- Indentation : 4 espaces (XML/CSS/JS), 2 espaces (JSON/YAML) + +## Fichiers +- Encodage : UTF-8 +- Fins de ligne : LF (Unix) +- Indentation : 4 espaces (Python, XML, CSS, JS), 2 espaces (JSON, YAML, RST, MD) +- Retour à la ligne final : oui +- Espaces en fin de ligne : supprimés + +## Git +- Branches : `develop` (développement), `master` (production) +- Pas de submodules Git — utilise **Google Repo** pour les addons +- Manifests XML dans `manifest/` pour chaque version Odoo +- Format de commit : `[TYPE] description` (ex: `[FIX]`, `[UPD]`, `[ADD]`, `[REM]`) diff --git a/.claude/rules/05-environments.md b/.claude/rules/05-environments.md new file mode 100644 index 0000000..cb8998a --- /dev/null +++ b/.claude/rules/05-environments.md @@ -0,0 +1,22 @@ +# Architecture des environnements virtuels + +``` +.venv.erplibre/ # Venv ERPLibre (outils : repo, poetry, coverage) +.venv.odoo18/ # Venv Odoo 18 (Python 3.12) +.venv.odoo17/ # Venv Odoo 17 (Python 3.10) +.venv.odoo16/ # Venv Odoo 16 (Python 3.10) +.venv.odoo14/ # Venv Odoo 14 (Python 3.8) +.venv.odoo12/ # Venv Odoo 12 (Python 3.7) +``` + +Géré via **pyenv** pour les multiples versions de Python. + +## Système de dépendances + +Chaque version Odoo a son propre ensemble dans `requirement/` : +- `pyproject.odooXX.0_pythonY.Z.toml` — Configuration Poetry +- `poetry.odooXX.0_pythonY.Z.lock` — Lock file Poetry +- `requirements.odooXX.0_pythonY.Z.txt` — Requirements pip (fallback) +- `ignore_requirements.odooXX.0.txt` — Paquets à ignorer + +Mise à jour : `./script/poetry/poetry_update.py` diff --git a/.claude/rules/06-code-generator.md b/.claude/rules/06-code-generator.md new file mode 100644 index 0000000..51ca59c --- /dev/null +++ b/.claude/rules/06-code-generator.md @@ -0,0 +1,9 @@ +# Code Generator + +ERPLibre inclut un système de génération de modules Odoo : +- `script/code_generator/new_project.py` — Créer un nouveau module +- `script/code_generator/create_from_existing_module.py` — Cloner un module existant +- `addons/TechnoLibre_odoo-code-generator/` — Moteur de génération +- `addons/TechnoLibre_odoo-code-generator-template/` — Templates + +Documentation : `doc/CODE_GENERATOR.md` diff --git a/.claude/rules/07-documentation.md b/.claude/rules/07-documentation.md new file mode 100644 index 0000000..ded55b5 --- /dev/null +++ b/.claude/rules/07-documentation.md @@ -0,0 +1,55 @@ +# Documentation multilingue + +La documentation est bilingue (anglais/français) via **mmg** (Multilingual Markdown Generator). + +## Fonctionnement +- Les fichiers sources sont les `.base.md` (contiennent les deux langues) +- `mmg` génère : `FICHIER.md` (anglais) et `FICHIER.fr.md` (français) +- Marqueurs : ``, ``, `` (blocs de code partagés) + +## Commandes +```bash +make doc_markdown # Regénérer toute la doc multilingue +``` + +## Convention +- **Ne jamais modifier directement** les fichiers `.md` ou `.fr.md` générés +- Toujours modifier le fichier `.base.md` correspondant, puis exécuter `make doc_markdown` +- Les blocs de code vont dans ``, le texte dans `` et `` +- En-tête obligatoire dans chaque `.base.md` : +``` + + + + +``` + +## Fichiers concernés (30 fichiers) +- Racine : `README`, `CHANGELOG`, `TODO` +- `doc/` : DEVELOPMENT, PRODUCTION, DISCOVER, RUN, MIGRATION, WINDOWS_INSTALLATION, FAQ, GIT_REPO, POETRY, RELEASE, UPDATE, CONTRIBUTION, HOWTO, TODO, CODE_GENERATOR +- `docker/` : README +- `script/*/` : database, deployment, fork_github_repo, nginx, restful, selenium (2), todo, odoo/migration +- `.github/ISSUE_TEMPLATE/` : bug_report, feature_request + +## Internationalisation du CLI TODO (i18n) + +Le CLI interactif `script/todo/todo.py` supporte le français et l'anglais. + +### Architecture +- **`script/todo/todo_i18n.py`** — Module de traduction (dictionnaire `TRANSLATIONS`, fonctions `t()`, `get_lang()`, `set_lang()`) +- Les chaînes traduisibles utilisent `t("clé")` au lieu de texte en dur +- Les entrées de `todo.json` peuvent avoir un champ `prompt_description_key` résolu via `t()` (fallback sur `prompt_description`) + +### Résolution de la langue (priorité) +1. Variable d'environnement `EL_LANG` (définie dans `env_var.sh`, défaut `"fr"`) +2. Défaut : `"fr"` + +### Comportement +- Première exécution : prompt bilingue demande à l'utilisateur de choisir sa langue +- Le choix est persisté dans `env_var.sh` +- Changement de langue possible via le menu Execute > Langue/Language + +### Ajouter une traduction +1. Ajouter la clé dans `TRANSLATIONS` de `todo_i18n.py` avec les valeurs `"fr"` et `"en"` +2. Remplacer la chaîne en dur par `t("ma_clé")` dans `todo.py` +3. Pour les entrées JSON : ajouter `"prompt_description_key": "ma_clé"` dans `todo.json` diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md new file mode 100644 index 0000000..d7f7935 --- /dev/null +++ b/.claude/rules/08-deployment.md @@ -0,0 +1,9 @@ +# Déploiement + +- **Docker** : `docker-compose.yml` (PostgreSQL 18 + PostGIS 3.6) +- **Systemd** : `script/systemd/` pour les services +- **Nginx** : `script/nginx/` pour le reverse proxy +- **SSL** : Certbot pour les certificats +- **DNS** : `script/deployment/update_dns_cloudflare.py` + +Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker). diff --git a/.claude/rules/09-workflow.md b/.claude/rules/09-workflow.md new file mode 100644 index 0000000..aa5f335 --- /dev/null +++ b/.claude/rules/09-workflow.md @@ -0,0 +1,49 @@ +# Workflow Orchestration + +## 1. Plan Mode Default +- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions) +- If something goes sideways, STOP and re-plan immediately +- Don't keep pushing. +- Use plan mode for verification steps, not just building +- Write detailed specs upfront to reduce ambiguity + +## 2. Subagent Strategy +- Use subagents liberally to keep the main context window clean. +- Offload research, exploration, and parallel analysis to subagents +- For complex problems, throw more compute at it via subagents +- One task per subagent for focused execution + +## 3. Self-Improvement Loop +- Capture Lessons: Update AGENT.md with any change in approach the user has asked you to make. +- Write rules for yourself that prevent the same mistake +- Ruthlessly iterate on these lessons until the mistake rate drops. +- Review lessons at session start for relevant project + +## 4. Verification Before Done +- Never mark a task complete without proving it works +- Diff behavior between main and your changes when relevant +- Ask yourself, "Would a staff engineer approve this?" +- Run tests, check logs, and demonstrate correctness. + +## 5. Demand Elegance (Balanced) +- For non-trivial changes, pause and ask, "Is there a more elegant way?" +- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution" +- Skip this for simple, obvious fixes +- Don't over-engineer. +- Challenge your own work before presenting it + +## 6. Autonomous Bug Fixing +- When given a bug report, just fix it. Don't ask for hand-holding +- Point at logs, errors, and failing tests. +- Then resolve them. +- Zero context switching required from the user +- Go fix failing CI tests without being told how + +## Task Management + +1. **Plan First**: Write a plan to `tasks/todo.md` with checkable items. +2. **Verify Plan**: Check in before starting implementation. +3. **Track Progress**: Mark items complete as you go. +4. **Explain Changes**: High-level summary at each step. +5. **Document Results**: Add review section to `tasks/todo.md` +6. **Capture Lessons**: Update `tasks/lessons.md` after corrections diff --git a/CLAUDE.md b/CLAUDE.md index b98adc8..5e5fc65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,256 +6,6 @@ ERPLibre est un fork communautaire d'Odoo Community Edition (OCE) supportant les Version actuelle : **1.6.0** | Licence : **AGPL-3.0+** Version Odoo par défaut : **18.0** (support officiel ERPLibre 1.6.0) -## Versions supportées - -| Odoo | Python | Poetry | Statut | -|-------|----------|--------|------------| -| 18.0 | 3.12.10 | 2.1.3 | **Défaut** | -| 17.0 | 3.10.18 | 1.8.3 | Actif | -| 16.0 | 3.10.18 | 1.8.3 | Actif | -| 15.0 | 3.8.20 | 1.8.3 | Déprécié | -| 14.0 | 3.8.20 | 1.5.0 | Déprécié | -| 13.0 | 3.7.17 | 1.5.0 | Déprécié | -| 12.0 | 3.7.17 | 1.5.0 | Déprécié | - -Configuration dans `conf/supported_version_erplibre.json`. -Fichiers de version : `.odoo-version`, `.erplibre-version`, `.poetry-version`, `.python-odoo-version`. - -## Structure du projet - -``` -erplibre/ -├── Makefile # Orchestrateur principal (inclut conf/make.*.Makefile) -├── run.sh / odoo_bin.sh # Lanceurs Odoo (venv + PYTHONPATH) -├── env_var.sh # Variables d'environnement globales -├── conf/ # Configuration : Makefiles modulaires, versions, manifests CSV -│ ├── make.installation.Makefile -│ ├── make.test.Makefile -│ ├── make.database.Makefile -│ ├── make.docker.Makefile -│ ├── make.code_generator.Makefile -│ ├── make.installation.poetry.Makefile -│ └── supported_version_erplibre.json -├── manifest/ # Manifests Google Repo (XML) par version Odoo -│ └── git_manifest_odoo{12..18}.0.xml -├── requirement/ # Dépendances par version -│ ├── pyproject.odooXX.0_pythonY.Z.toml -│ ├── poetry.odooXX.0_pythonY.Z.lock -│ └── requirements.odooXX.0_pythonY.Z.txt -├── script/ # Scripts utilitaires (32+ catégories) -│ ├── todo/ # CLI interactif principal (todo.py) -│ ├── database/ # Opérations DB (restore, migrate, image_db) -│ ├── addons/ # Gestion des modules (install, update, uninstall) -│ ├── code_generator/ # Génération de modules Odoo -│ ├── version/ # Changement de version -│ ├── git/ # Opérations Git et Google Repo -│ ├── maintenance/ # Formatage (black, isort, prettier) -│ ├── test/ # Tests parallèles + coverage -│ ├── docker/ # Build/run Docker -│ ├── poetry/ # Gestion Poetry -│ ├── deployment/ # Déploiement production -│ └── selenium/ # Tests web automatisés -├── docker/ # Dockerfiles + docker-compose par version -├── addons/ # Répertoire des addons (géré par Google Repo) -│ ├── OCA_*/ # Modules OCA -│ ├── ERPLibre_*/ # Modules ERPLibre -│ ├── TechnoLibre_*/ # Code generator + templates -│ └── MathBenTech_*/ # Modules spécialisés -├── odoo{12..18}.0/ # Sources Odoo par version -├── doc/ # Documentation (DEVELOPMENT, PRODUCTION, MIGRATION, etc.) -├── test/ # Framework de test -└── private/ # Fichiers privés (non versionné) -``` - -## Commandes essentielles - -### Changement de version Odoo -```bash -make switch_odoo_18 # Passer à Odoo 18 -make switch_odoo_17 # Passer à Odoo 17 -make switch_odoo_16 # Passer à Odoo 16 (défaut) -make switch_odoo_15 # Passer à Odoo 15 -make switch_odoo_14 # Passer à Odoo 14 -make switch_odoo_13 # Passer à Odoo 13 -make switch_odoo_12 # Passer à Odoo 12 -make switch_odoo_16_update # Passer + mettre à jour les dépendances -``` - -### Exécution -```bash -make run # Lancer Odoo (port 8069) -make run_test # Lancer avec la DB "test" -./run.sh -d ma_base # Lancer avec une DB spécifique -./odoo_bin.sh [args] # Exécuter odoo-bin directement -``` - -### Base de données -```bash -make db_create_db_test # Créer la DB "test" -make db_clone_test_to_test2 # Cloner test -> test2 -./script/database/db_restore.py --database NOM --image IMAGE -./script/addons/install_addons_dev.sh DB module1,module2 -./script/addons/update_addons_all.sh DB -``` - -### Tests -```bash -make test # Tests de base + format -make test_full_fast # Tests complets en parallèle -make test_full_fast_coverage # Avec couverture de code -``` - -### Formatage du code -```bash -make format # Fichiers à commiter uniquement -make format_all # Tout formater (parallèle) -``` - -Outils : **Black** (Python, ligne max 79), **isort** (profil black, ligne max 79), **Prettier** (XML/JSON), **Flake8** (ligne max 80, complexité max 16). - -### Docker -```bash -make docker_run_daemon # Lancer en production -make docker_build_odoo_18 # Builder l'image Odoo 18 -``` - -### Google Repo (gestion multi-dépôts) -```bash -make repo_show_status # Statut de tous les dépôts addons -make repo_configure_all # Configurer tous les dépôts -make repo_do_stash # Stash tous les dépôts -.venv.erplibre/bin/repo sync # Synchroniser les dépôts -``` - -### Installation -```bash -make install_os # Dépendances système -make install_odoo_18 # Installer Odoo 18 complet -``` - -## Conventions de code - -### Python -- Formateur : **Black** (profil par défaut, ligne max 79 pour les modules Odoo) -- Imports : **isort** avec profil `black`, longueur de ligne 79 -- Linting : **Flake8** avec bugbear, max-line-length 80, max-complexity 16 -- Ignorer : E203, E501, W503 (compatibilité Black) - -### XML / JSON / YAML -- Formateur : **Prettier** (via npm) -- Indentation : 4 espaces (XML/CSS/JS), 2 espaces (JSON/YAML) - -### Fichiers -- Encodage : UTF-8 -- Fins de ligne : LF (Unix) -- Indentation : 4 espaces (Python, XML, CSS, JS), 2 espaces (JSON, YAML, RST, MD) -- Retour à la ligne final : oui -- Espaces en fin de ligne : supprimés - -### Git -- Branches : `develop` (développement), `master` (production) -- Pas de submodules Git — utilise **Google Repo** pour les addons -- Manifests XML dans `manifest/` pour chaque version Odoo - -## Architecture des environnements virtuels - -``` -.venv.erplibre/ # Venv ERPLibre (outils : repo, poetry, coverage) -.venv.odoo18/ # Venv Odoo 18 (Python 3.12) -.venv.odoo17/ # Venv Odoo 17 (Python 3.10) -.venv.odoo16/ # Venv Odoo 16 (Python 3.10) -.venv.odoo14/ # Venv Odoo 14 (Python 3.8) -.venv.odoo12/ # Venv Odoo 12 (Python 3.7) -``` - -Géré via **pyenv** pour les multiples versions de Python. - -## Système de dépendances - -Chaque version Odoo a son propre ensemble dans `requirement/` : -- `pyproject.odooXX.0_pythonY.Z.toml` — Configuration Poetry -- `poetry.odooXX.0_pythonY.Z.lock` — Lock file Poetry -- `requirements.odooXX.0_pythonY.Z.txt` — Requirements pip (fallback) -- `ignore_requirements.odooXX.0.txt` — Paquets à ignorer - -Mise à jour : `./script/poetry/poetry_update.py` - -## Code Generator - -ERPLibre inclut un système de génération de modules Odoo : -- `script/code_generator/new_project.py` — Créer un nouveau module -- `script/code_generator/create_from_existing_module.py` — Cloner un module existant -- `addons/TechnoLibre_odoo-code-generator/` — Moteur de génération -- `addons/TechnoLibre_odoo-code-generator-template/` — Templates - -Documentation : `doc/CODE_GENERATOR.md` - -## Documentation multilingue - -La documentation est bilingue (anglais/français) via **mmg** (Multilingual Markdown Generator). - -### Fonctionnement -- Les fichiers sources sont les `.base.md` (contiennent les deux langues) -- `mmg` génère : `FICHIER.md` (anglais) et `FICHIER.fr.md` (français) -- Marqueurs : ``, ``, `` (blocs de code partagés) - -### Commandes -```bash -make doc_markdown # Regénérer toute la doc multilingue -``` - -### Convention -- **Ne jamais modifier directement** les fichiers `.md` ou `.fr.md` générés -- Toujours modifier le fichier `.base.md` correspondant, puis exécuter `make doc_markdown` -- Les blocs de code vont dans ``, le texte dans `` et `` -- En-tête obligatoire dans chaque `.base.md` : -``` - - - - -``` - -### Fichiers concernés (30 fichiers) -- Racine : `README`, `CHANGELOG`, `TODO` -- `doc/` : DEVELOPMENT, PRODUCTION, DISCOVER, RUN, MIGRATION, WINDOWS_INSTALLATION, FAQ, GIT_REPO, POETRY, RELEASE, UPDATE, CONTRIBUTION, HOWTO, TODO, CODE_GENERATOR -- `docker/` : README -- `script/*/` : database, deployment, fork_github_repo, nginx, restful, selenium (2), todo, odoo/migration -- `.github/ISSUE_TEMPLATE/` : bug_report, feature_request - -## Internationalisation du CLI TODO (i18n) - -Le CLI interactif `script/todo/todo.py` supporte le français et l'anglais. - -### Architecture -- **`script/todo/todo_i18n.py`** — Module de traduction (dictionnaire `TRANSLATIONS`, fonctions `t()`, `get_lang()`, `set_lang()`) -- Les chaînes traduisibles utilisent `t("clé")` au lieu de texte en dur -- Les entrées de `todo.json` peuvent avoir un champ `prompt_description_key` résolu via `t()` (fallback sur `prompt_description`) - -### Résolution de la langue (priorité) -1. Variable d'environnement `EL_LANG` (définie dans `env_var.sh`, défaut `"fr"`) -2. Défaut : `"fr"` - -### Comportement -- Première exécution : prompt bilingue demande à l'utilisateur de choisir sa langue -- Le choix est persisté dans `env_var.sh` -- Changement de langue possible via le menu Execute > Langue/Language - -### Ajouter une traduction -1. Ajouter la clé dans `TRANSLATIONS` de `todo_i18n.py` avec les valeurs `"fr"` et `"en"` -2. Remplacer la chaîne en dur par `t("ma_clé")` dans `todo.py` -3. Pour les entrées JSON : ajouter `"prompt_description_key": "ma_clé"` dans `todo.json` - -## Déploiement - -- **Docker** : `docker-compose.yml` (PostgreSQL 18 + PostGIS 3.6) -- **Systemd** : `script/systemd/` pour les services -- **Nginx** : `script/nginx/` pour le reverse proxy -- **SSL** : Certbot pour les certificats -- **DNS** : `script/deployment/update_dns_cloudflare.py` - -Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker). - ## Points d'attention pour Claude - Toujours vérifier la version Odoo active avant de modifier du code (`cat .odoo-version`) @@ -270,58 +20,24 @@ Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch L - Pour la documentation : modifier les `.base.md`, jamais les `.md` ou `.fr.md` directement - Outil mmg disponible via `source .venv.erplibre/bin/activate && mmg` -## Workflow Orchestration - -### 1. Plan Mode Default -- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions) -- If something goes sideways, STOP and re-plan immediately -- Don't keep pushing. -- Use plan mode for verification steps, not just building -- Write detailed specs upfront to reduce ambiguity - -### 2. Subagent Strategy -- Use subagents liberally to keep the main context window clean. -- Offload research, exploration, and parallel analysis to subagents -- For complex problems, throw more compute at it via subagents -- One task per subagent for focused execution - -### 3. Self-Improvement Loop -- Capture Lessons: Update AGENT.md with any change in approach the user has asked you to make. -- Write rules for yourself that prevent the same mistake -- Ruthlessly iterate on these lessons until the mistake rate drops. -- Review lessons at session start for relevant project - -### 4. Verification Before Done -- Never mark a task complete without proving it works -- Diff behavior between main and your changes when relevant -- Ask yourself, "Would a staff engineer approve this?" -- Run tests, check logs, and demonstrate correctness. - -### 5. Demand Elegance (Balanced) -- For non-trivial changes, pause and ask, "Is there a more elegant way?" -- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution" -- Skip this for simple, obvious fixes -- Don't over-engineer. -- Challenge your own work before presenting it - -### 6. Autonomous Bug Fixing -- When given a bug report, just fix it. Don't ask for hand-holding -- Point at logs, errors, and failing tests. -- Then resolve them. -- Zero context switching required from the user -- Go fix failing CI tests without being told how - -## Task Management - -1. **Plan First**: Write a plan to `tasks/todo.md` with checkable items. -2. **Verify Plan**: Check in before starting implementation. -3. **Track Progress**: Mark items complete as you go. -4. **Explain Changes**: High-level summary at each step. -5. **Document Results**: Add review section to `tasks/todo.md` -6. **Capture Lessons**: Update `tasks/lessons.md` after corrections - ## Core Principles - **Simplicity First**: Make every change as simple as possible. Impact minimal code. - **No Laziness**: Find root causes. No temporary fixes. Senior developer standards. - **Minimal Impact**: Changes should only touch what's necessary. Avoid introducing bugs. + +## Règles détaillées + +Les instructions détaillées sont dans `.claude/rules/` : + +| Fichier | Contenu | +|---------|---------| +| `01-versions.md` | Versions Odoo/Python/Poetry supportées | +| `02-project-structure.md` | Arborescence du projet | +| `03-commands.md` | Commandes essentielles (make, scripts) | +| `04-code-conventions.md` | Conventions Python, XML, fichiers, Git | +| `05-environments.md` | Venvs, pyenv, système de dépendances | +| `06-code-generator.md` | Génération de modules Odoo | +| `07-documentation.md` | Documentation multilingue (mmg) + i18n CLI | +| `08-deployment.md` | Docker, systemd, nginx, SSL, DNS | +| `09-workflow.md` | Workflow orchestration + task management | diff --git a/conf/template_claude_commands_todo_add_command.md b/conf/template_claude_commands_todo_add_command.md new file mode 100644 index 0000000..dce3469 --- /dev/null +++ b/conf/template_claude_commands_todo_add_command.md @@ -0,0 +1,143 @@ +--- +name: todo_add_command +description: "Add a new menu command to script/todo/todo.py with i18n support and optional todo.json entry." +allowed-tools: + - Read + - Edit + - Write + - Grep + - Glob + - Bash(python3 -m py_compile:*) + - Bash(python3 -c:*) +--- + +## Context + +- Current todo.py menu structure: !`grep -n "def prompt_execute" script/todo/todo.py | head -20` +- Current todo.json sections: !`python3 -c "import json; d=json.load(open('script/todo/todo.json')); print('\n'.join(d.keys()))"` +- Current i18n keys count: !`grep -c fr.: script/todo/todo_i18n.py` + +## Architecture Reference + +### Files to modify + +| File | Role | +|------|------| +| `script/todo/todo.py` | Main CLI — menu methods, business logic | +| `script/todo/todo_i18n.py` | Translations dict `TRANSLATIONS` with `"fr"` and `"en"` keys | +| `script/todo/todo.json` | Config-driven entries (optional, for `bash_command` or `makefile_cmd`) | + +### Pattern A — Hardcoded menu entry (interactive logic) + +Used when the command needs Python logic (user prompts, conditionals, API calls). + +1. **Add i18n keys** in `todo_i18n.py` `TRANSLATIONS` dict: +```python +"my_feature_description": { + "fr": "Description en français", + "en": "English description", +}, +``` + +2. **Add choice** in the parent menu method (e.g. `prompt_execute_git`): +```python +choices = [ + ..., + {"prompt_description": t("my_feature_description")}, +] +``` + +3. **Add elif branch** in the same menu's while loop: +```python +elif status == "N": + self._my_feature() +``` + +4. **Add method** to the `TODO` class: +```python +def _my_feature(self): + # Implementation here + pass +``` + +### Pattern B — Config-driven entry (simple bash command) + +Used when the command just runs a bash command or a make target. + +1. **Add i18n key** in `todo_i18n.py` (use `prompt_description_key`). + +2. **Add entry** in `todo.json` under the appropriate `*_from_makefile` section: +```json +{ + "prompt_description_key": "my_i18n_key", + "bash_command": "my-command --flag" +} +``` +Or for make targets: +```json +{ + "prompt_description_key": "my_i18n_key", + "makefile_cmd": "my_make_target" +} +``` + +These are automatically picked up by `execute_from_configuration()`. + +### Available menu sections + +| Menu | Method | JSON key | +|------|--------|----------| +| Automation | `prompt_execute_function` | `function` | +| Code | `prompt_execute_code` | `code_from_makefile` | +| Config | `prompt_execute_config` | — | +| Database | `prompt_execute_database` | — | +| Doc | `prompt_execute_doc` | — | +| Git | `prompt_execute_git` | `git_from_makefile` | +| GPT code | `prompt_execute_gpt_code` | — | +| Network | `prompt_execute_network` | — | +| Process | `prompt_execute_process` | — | +| Run | `prompt_execute_instance` | `instance` | +| Security | `prompt_execute_security` | — | +| Test | `prompt_execute_test` | — | +| Update | `prompt_execute_update` | `update_from_makefile` | + +### Key conventions + +- i18n keys: `snake_case`, grouped by section with a comment header +- Method names: `_private_method` for actions, `prompt_execute_*` for submenus +- All user-facing strings must use `t("key")` — never hardcoded text +- Use `self.execute.exec_command_live(cmd, source_erplibre=False)` to run bash +- Use `input(t("prompt_key")).strip()` for user input +- Use `click.prompt(help_info)` for menu navigation +- Menu methods return `False` on back, use `self.fill_help_info(choices)` for formatting + +## Task + +Add a new command to the todo.py menu system. The user will describe what they want. + +### Steps + +1. **Determine the target menu section** from the user's description +2. **Choose Pattern A or B** based on complexity: + - Simple bash/make command → Pattern B (todo.json entry) + - Needs user interaction or logic → Pattern A (hardcoded method) +3. **Add i18n translations** in `todo_i18n.py` — always both `"fr"` and `"en"` +4. **Implement the feature** following the appropriate pattern +5. **Validate syntax**: + ```bash + python3 -m py_compile script/todo/todo.py + python3 -m py_compile script/todo/todo_i18n.py + python3 -c "import json; json.load(open('script/todo/todo.json'))" + ``` + +### Rules + +- Keep menu numbering sequential — update all `elif` branches if inserting +- Group i18n keys with a `# Section name` comment +- Match existing code style (Black, 79 char lines for Odoo modules) +- Do not break existing menu entries or their numbering +- Test that `fill_help_info` and `execute_from_configuration` handle the new entry + +## User Request + +$ARGUMENTS diff --git a/script/code/git_commit_migration_addons_path.py b/script/code/git_commit_migration_addons_path.py index a725200..761a308 100755 --- a/script/code/git_commit_migration_addons_path.py +++ b/script/code/git_commit_migration_addons_path.py @@ -96,9 +96,7 @@ def commit_by_directory(): run_git_command(f'cd "{config.path}" && git rm -r {directory}') # Crée le commit - commit_message = ( - f"[{mig_prefix_msg}] {directory}: Migration to {config.odoo_version}" - ) + commit_message = f"[{mig_prefix_msg}] {directory}: Migration to {config.odoo_version}" commit_result = run_git_command( f'cd "{config.path}" && git commit -m "{commit_message}"' ) diff --git a/script/database/list_remote.py b/script/database/list_remote.py index d224c30..9c1e22a 100755 --- a/script/database/list_remote.py +++ b/script/database/list_remote.py @@ -10,28 +10,31 @@ def get_db_list_xmlrpc(odoo_url): Retrieves the list of Odoo databases using the XML-RPC API. """ try: - common = xmlrpc.client.ServerProxy(f'{odoo_url}/xmlrpc/db') + common = xmlrpc.client.ServerProxy(f"{odoo_url}/xmlrpc/db") db_list = common.list() return db_list except xmlrpc.client.Fault as e: - print(f"XML-RPC Error: {e.faultCode} - {e.faultString}", file=sys.stderr) + print( + f"XML-RPC Error: {e.faultCode} - {e.faultString}", file=sys.stderr + ) return [] except Exception as e: print(f"Connection Error: {e}", file=sys.stderr) return [] + # --- CLI using Click --- @click.command() @click.option( - '--odoo-url', - default='http://localhost:8069', - help='URL of the Odoo server.', - show_default=True + "--odoo-url", + default="http://localhost:8069", + help="URL of the Odoo server.", + show_default=True, ) @click.option( - '--raw', + "--raw", is_flag=True, - help='Output one database per line, without extra formatting. Useful for scripting.' + help="Output one database per line, without extra formatting. Useful for scripting.", ) def list_databases(odoo_url, raw=False): """ @@ -56,5 +59,5 @@ def list_databases(odoo_url, raw=False): # --- Script Execution --- -if __name__ == '__main__': +if __name__ == "__main__": list_databases() diff --git a/script/execute/execute.py b/script/execute/execute.py index 887a8e9..62ddcb8 100644 --- a/script/execute/execute.py +++ b/script/execute/execute.py @@ -115,7 +115,7 @@ class Execute: command = self.cmd_source_default % command if not quiet: - print("🏠 ⬇ Execute command :") + print("🏠 ⬇ Execute command :\n") print(command) output_lines = [] @@ -172,10 +172,10 @@ class Execute: duration_delta = datetime.timedelta(seconds=duration_sec) human_time = humanize.precisedelta(duration_delta) if not quiet: - print(f"🏠 ⬆ Executed ({human_time}) :") + print(f"🏠 ⬆ Executed ({human_time}) :\n") else: if not quiet: - print(f"🏠 ⬆ Executed ({duration_sec:.2f} sec.) :") + print(f"🏠 ⬆ Executed ({duration_sec:.2f} sec.) :\n") if not quiet: print(command) print() diff --git a/script/git/git_local_server.py b/script/git/git_local_server.py index 7fcc584..3f1803b 100755 --- a/script/git/git_local_server.py +++ b/script/git/git_local_server.py @@ -151,8 +151,7 @@ Use --production-ready for /srv/git (requires root). type=int, default=DEFAULT_JOBS, help=( - "Parallel jobs for init/remote/push" - f" (default: {DEFAULT_JOBS})" + "Parallel jobs for init/remote/push" f" (default: {DEFAULT_JOBS})" ), ) parser.add_argument( @@ -247,11 +246,7 @@ def get_erplibre_root_project(erplibre_root): capture_output=True, text=True, ) - revision = ( - result.stdout.strip() - if result.returncode == 0 - else "master" - ) + revision = result.stdout.strip() if result.returncode == 0 else "master" return { "name": ERPLIBRE_REPO_NAME, @@ -278,19 +273,13 @@ async def _init_single_bare_repo(git_path, project, semaphore): return "skipped" _logger.info(f" Creating: {bare_path}") - _, err, rc = await _run_git( - "git", "init", "--bare", bare_path - ) + _, err, rc = await _run_git("git", "init", "--bare", bare_path) if rc != 0: - _logger.warning( - f" Init failed: {bare_path}: {err.strip()}" - ) + _logger.warning(f" Init failed: {bare_path}: {err.strip()}") return "error" # Enable git daemon export - export_file = os.path.join( - bare_path, "git-daemon-export-ok" - ) + export_file = os.path.join(bare_path, "git-daemon-export-ok") open(export_file, "w").close() # Allow pushing from shallow clones @@ -312,10 +301,7 @@ async def init_bare_repos(git_path, projects, jobs): semaphore = asyncio.Semaphore(jobs) results = await asyncio.gather( - *[ - _init_single_bare_repo(git_path, p, semaphore) - for p in projects - ] + *[_init_single_bare_repo(git_path, p, semaphore) for p in projects] ) created = results.count("created") @@ -352,9 +338,7 @@ async def _add_single_remote( ): """Add or update remote for a single repo (async).""" async with semaphore: - repo_path = os.path.join( - erplibre_root, project["path"] - ) + repo_path = os.path.join(erplibre_root, project["path"]) if not os.path.isdir(repo_path): _logger.warning(f" Not found: {repo_path}") return "error" @@ -368,9 +352,7 @@ async def _add_single_remote( ) # Check if remote already exists - stdout, _, _ = await _run_git( - "git", "-C", repo_path, "remote" - ) + stdout, _, _ = await _run_git("git", "-C", repo_path, "remote") existing_remotes = stdout.strip().split("\n") if remote_name in existing_remotes: @@ -403,8 +385,7 @@ async def _add_single_remote( ) if rc != 0: _logger.warning( - f" add failed for" - f" {project['path']}: {err.strip()}" + f" add failed for" f" {project['path']}: {err.strip()}" ) return "error" _logger.info(f" Added: {project['path']}") @@ -447,10 +428,7 @@ async def add_remotes( added = results.count("added") updated = results.count("updated") errors = results.count("error") - print( - f"Remotes: {added} added, {updated} updated," - f" {errors} errors" - ) + print(f"Remotes: {added} added, {updated} updated," f" {errors} errors") # --- Async workers for push --- @@ -458,9 +436,7 @@ async def add_remotes( async def _is_detached_head(repo_path): """Check if a repo is in detached HEAD state.""" - _, _, rc = await _run_git( - "git", "-C", repo_path, "symbolic-ref", "HEAD" - ) + _, _, rc = await _run_git("git", "-C", repo_path, "symbolic-ref", "HEAD") return rc != 0 @@ -471,9 +447,7 @@ async def _checkout_manifest_branch(repo_path, revision): on the exact commit. We create/checkout a local branch matching the manifest revision so git push works. """ - branch = ( - revision.split("/")[-1] if "/" in revision else revision - ) + branch = revision.split("/")[-1] if "/" in revision else revision # Check if local branch already exists _, _, rc = await _run_git( @@ -485,9 +459,7 @@ async def _checkout_manifest_branch(repo_path, revision): f"refs/heads/{branch}", ) if rc == 0: - await _run_git( - "git", "-C", repo_path, "checkout", branch - ) + await _run_git("git", "-C", repo_path, "checkout", branch) else: await _run_git( "git", @@ -514,9 +486,7 @@ async def _update_bare_head(git_path, project): revision = project.get("revision", "") if not revision: return - branch = ( - revision.split("/")[-1] if "/" in revision else revision - ) + branch = revision.split("/")[-1] if "/" in revision else revision await _run_git( "git", "-C", @@ -529,20 +499,10 @@ async def _update_bare_head(git_path, project): async def _try_unshallow(repo_path, project, remote_name): """Try to unshallow a repo by fetching full history.""" - shallow_file = os.path.join( - repo_path, ".git", "shallow" - ) - _logger.info( - f" Unshallowing {project['path']}..." - ) - stdout, _, _ = await _run_git( - "git", "-C", repo_path, "remote" - ) - remotes = [ - r - for r in stdout.strip().split("\n") - if r and r != remote_name - ] + shallow_file = os.path.join(repo_path, ".git", "shallow") + _logger.info(f" Unshallowing {project['path']}...") + stdout, _, _ = await _run_git("git", "-C", repo_path, "remote") + remotes = [r for r in stdout.strip().split("\n") if r and r != remote_name] manifest_remote = project.get("remote", "") if manifest_remote in remotes: remotes.remove(manifest_remote) @@ -562,9 +522,7 @@ async def _try_unshallow(repo_path, project, remote_name): except asyncio.TimeoutError: continue if not os.path.exists(shallow_file): - _logger.info( - f" Unshallowed via {try_remote}" - ) + _logger.info(f" Unshallowed via {try_remote}") return if os.path.exists(shallow_file): _logger.warning( @@ -585,30 +543,22 @@ async def _push_single_repo( ): """Push a single repo to local remote (async).""" async with semaphore: - repo_path = os.path.join( - erplibre_root, project["path"] - ) + repo_path = os.path.join(erplibre_root, project["path"]) if not os.path.isdir(repo_path): _logger.warning(f" Not found: {repo_path}") return "error", False # Handle shallow repos - shallow_file = os.path.join( - repo_path, ".git", "shallow" - ) + shallow_file = os.path.join(repo_path, ".git", "shallow") if os.path.exists(shallow_file): if unshallow: - await _try_unshallow( - repo_path, project, remote_name - ) + await _try_unshallow(repo_path, project, remote_name) else: # Enable shallow push on bare repo repo_name = project["name"] if not repo_name.endswith(".git"): repo_name += ".git" - bare_path = os.path.join( - git_path, repo_name - ) + bare_path = os.path.join(git_path, repo_name) if os.path.isdir(bare_path): await _run_git( "git", @@ -618,19 +568,14 @@ async def _push_single_repo( "receive.shallowUpdate", "true", ) - _logger.info( - f" Shallow push for" - f" {project['path']}" - ) + _logger.info(f" Shallow push for" f" {project['path']}") # Handle detached HEAD: checkout manifest branch did_checkout = False if await _is_detached_head(repo_path): revision = project.get("revision", "") if revision: - branch = await _checkout_manifest_branch( - repo_path, revision - ) + branch = await _checkout_manifest_branch(repo_path, revision) _logger.info( f" Checkout {branch} for" f" {project['path']}" @@ -663,9 +608,7 @@ async def _push_single_repo( "push", remote_name, ] - _, err, rc = await _run_git( - *cmd, timeout=120 - ) + _, err, rc = await _run_git(*cmd, timeout=120) if rc != 0: _logger.warning( f" Push failed for" @@ -675,14 +618,10 @@ async def _push_single_repo( return "error", did_checkout else: await _update_bare_head(git_path, project) - _logger.info( - f" Pushed: {project['path']}" - ) + _logger.info(f" Pushed: {project['path']}") return "pushed", did_checkout except asyncio.TimeoutError: - _logger.warning( - f" Timeout pushing {project['path']}" - ) + _logger.warning(f" Timeout pushing {project['path']}") return "error", did_checkout @@ -744,8 +683,7 @@ def print_clone_commands(git_path, projects, port): if clone_path == ".": clone_path = "erplibre" lines.append( - f" git clone {base_url}/{repo_name}" - f" {clone_path}" + f" git clone {base_url}/{repo_name}" f" {clone_path}" ) lines.sort() for line in lines: diff --git a/script/git/github_api.py b/script/git/github_api.py index 2431c76..9ebbadf 100644 --- a/script/git/github_api.py +++ b/script/git/github_api.py @@ -26,17 +26,10 @@ def get_pull_request_repo( parsed_url = parse(upstream_url) status, user = gh.user.get() - user_name = ( - user["login"] if not organization_name else organization_name - ) - status, lst_pull = ( - gh.repos[user_name][parsed_url.repo].pulls.get() - ) + user_name = user["login"] if not organization_name else organization_name + status, lst_pull = gh.repos[user_name][parsed_url.repo].pulls.get() if type(lst_pull) is dict: - print( - f"For url {upstream_url}," - f" got {lst_pull.get('message')}" - ) + print(f"For url {upstream_url}," f" got {lst_pull.get('message')}") return False else: for pull in lst_pull: @@ -53,26 +46,21 @@ def fork_repo( parsed_url = parse(upstream_url) status, user = gh.user.get() - user_name = ( - user["login"] if not organization_name else organization_name - ) - status, forked_repo = ( - gh.repos[user_name][parsed_url.repo].get() - ) + user_name = user["login"] if not organization_name else organization_name + status, forked_repo = gh.repos[user_name][parsed_url.repo].get() if status == 404: - status, upstream_repo = ( - gh.repos[parsed_url.owner][parsed_url.repo].get() - ) + status, upstream_repo = gh.repos[parsed_url.owner][ + parsed_url.repo + ].get() if status == 404: print("Unable to find repo %s" % upstream_url) exit(1) args = {} if organization_name: args["organization"] = organization_name - status, forked_repo = ( - gh.repos[parsed_url.owner][parsed_url.repo] - .forks.post(**args) - ) + status, forked_repo = gh.repos[parsed_url.owner][ + parsed_url.repo + ].forks.post(**args) if status == 404: print( f"{Fore.RED}Error{Style.RESET_ALL} when forking" @@ -82,23 +70,16 @@ def fork_repo( else: try: print( - "Forked %s to %s" - % (upstream_url, forked_repo["html_url"]) + "Forked %s to %s" % (upstream_url, forked_repo["html_url"]) ) except Exception as e: print(e) print(forked_repo) print(upstream_url) elif status == 202: - print( - "Forked repo %s already exists" - % forked_repo["full_name"] - ) + print("Forked repo %s already exists" % forked_repo["full_name"]) elif status != 200: - print( - "Status not supported: %s - %s" - % (status, forked_repo) - ) + print("Status not supported: %s - %s" % (status, forked_repo)) exit(1) @@ -110,9 +91,7 @@ def add_and_fetch_remote( """ try: working_repo = Repo(repo_info.relative_path) - if repo_info.organization in [ - a.name for a in working_repo.remotes - ]: + if repo_info.organization in [a.name for a in working_repo.remotes]: print( f'Remote "{repo_info.organization}" already exist' f" in {repo_info.relative_path}" @@ -122,8 +101,7 @@ def add_and_fetch_remote( print(f"New repo {repo_info.relative_path}") if not root_repo: print( - "Missing git repository to root for repo" - f" {repo_info.path}" + "Missing git repository to root for repo" f" {repo_info.path}" ) return if branch_name: @@ -149,16 +127,14 @@ def add_and_fetch_remote( # Add remote upstream_remote = retry( wait_exponential_multiplier=1000, stop_max_delay=15000 - )(working_repo.create_remote)( - repo_info.organization, repo_info.url_https - ) + )(working_repo.create_remote)(repo_info.organization, repo_info.url_https) print( 'Remote "%s" created for %s' % (repo_info.organization, repo_info.url_https) ) # Fetch the remote - retry( - wait_exponential_multiplier=1000, stop_max_delay=15000 - )(upstream_remote.fetch)() + retry(wait_exponential_multiplier=1000, stop_max_delay=15000)( + upstream_remote.fetch + )() print('Remote "%s" fetched' % repo_info.organization) diff --git a/script/ide/pycharm_configuration.py b/script/ide/pycharm_configuration.py index 45dda12..c582d04 100755 --- a/script/ide/pycharm_configuration.py +++ b/script/ide/pycharm_configuration.py @@ -26,7 +26,9 @@ _logger = logging.getLogger(__name__) PROJECT_NAME = os.path.basename(os.getcwd()) IDEA_PATH = "./.idea" -DEFAULT_ODOO_BIN = "./odoo/odoo-bin" # Need this to replace static configuration +DEFAULT_ODOO_BIN = ( + "./odoo/odoo-bin" # Need this to replace static configuration +) IDEA_MISC = os.path.join(IDEA_PATH, "misc.xml") IDEA_WORKSPACE = os.path.join(IDEA_PATH, "workspace.xml") VCS_WORKSPACE = os.path.join(IDEA_PATH, "vcs.xml") diff --git a/script/odoo/util/show_installed_module.py b/script/odoo/util/show_installed_module.py index 8de0454..df41542 100644 --- a/script/odoo/util/show_installed_module.py +++ b/script/odoo/util/show_installed_module.py @@ -2,7 +2,9 @@ # © 2021-2026 TechnoLibre (http://www.technolibre.ca) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -installed_modules = env["ir.module.module"].search([('state', '=', 'installed')]) +installed_modules = env["ir.module.module"].search( + [("state", "=", "installed")] +) print("Installed modules:") diff --git a/script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py b/script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py index c137af0..ccd2a8a 100644 --- a/script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py +++ b/script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py @@ -10,18 +10,20 @@ print("Running a script in the Odoo shell!") # ERROR: insert or update on table "discuss_channel_member" violates foreign key constraint "discuss_channel_member_channel_id_fkey" # DÉTAIL : Key (channel_id)=(3) is not present in table "discuss_channel". -env.cr.execute(""" +env.cr.execute( + """ ALTER TABLE discuss_channel DROP CONSTRAINT discuss_channel_channel_type_not_null; -""") +""" +) env.cr.commit() -#env.cr.execute("SELECT to_regclass('public.discuss_channel');") -#print("table:", env.cr.fetchone()) +# env.cr.execute("SELECT to_regclass('public.discuss_channel');") +# print("table:", env.cr.fetchone()) -#print( +# print( # "model discuss.channel:", # env["ir.model"].search([("model", "=", "discuss.channel")]), -#) +# ) print("End fix migration with update mail postgresql 17 to postgresql 18.") diff --git a/script/todo/todo.json b/script/todo/todo.json index 90a52a7..27255b0 100644 --- a/script/todo/todo.json +++ b/script/todo/todo.json @@ -10,47 +10,52 @@ }, "instance": [ { - "prompt_description_key": "json_instance_test", + "prompt_description_key": "Test - Minimal base instance", "makefile_cmd": "db_restore_erplibre_base_db_test", "database": "test" }, { - "prompt_description_key": "json_robot_minimal", + "prompt_description_key": "Open RobotLibre 🤖 minimal", "makefile_cmd": "robot_libre", "database": "robotlibre" }, { - "prompt_description_key": "json_robot_search", + "prompt_description_key": "Open RobotLibre 🤖 with search enabled", "makefile_cmd": "robot_libre_all", "database": "robotlibre" } ], "function": [ { - "prompt_description_key": "json_open_erplibre_todo", + "prompt_description_key": "Open ERPLibre with TODO 🤖", "command": "./.venv.erplibre/bin/python ./script/selenium/web_login.py" } ], "update_from_makefile": [ { - "prompt_description_key": "json_update_erplibre_base_test", + "prompt_description_key": "Update all erplibre_base on database test", "makefile_cmd": "db_erplibre_base_db_test_update_all", "database": "test" } ], "code_from_makefile": [ { - "prompt_description_key": "json_show_code_status", + "prompt_description_key": "Show code status", "makefile_cmd": "repo_show_status" }, { - "prompt_description_key": "json_stash_all_code", + "prompt_description_key": "Stash all code", "makefile_cmd": "repo_do_stash" }, { - "prompt_description_key": "json_format_modified_code", + "prompt_description_key": "Format modified code", "makefile_cmd": "format" } ], - "git_from_makefile": [] + "git_from_makefile": [ + { + "prompt_description_key": "Configure git local editor to vim", + "bash_command": "git config --global core.editor \"vim\"" + } + ] } diff --git a/script/todo/todo.py b/script/todo/todo.py index 16e758a..b099569 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -41,9 +41,6 @@ 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 @@ -103,33 +100,33 @@ class TODO: 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')}") + print(t("Choose language / Choisir la langue") + ":") + print(f"[1] {t('French')}") + print(f"[2] {t('English')}") + print(f"[0] {t('Back')}") choice = "" while choice not in ("0", "1", "2"): - choice = input(t("selection")).strip() + choice = input(t("Select: ")).strip() if choice == "0": return False elif choice == "1": set_lang("fr") else: set_lang("en") - print(t("lang_changed")) + print(t("Language changed to: English")) 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")} + print(t("Opening TODO ...")) + print(f"🤖 {t('=> Enter your choice by number and press Enter!')}") + help_info = f"""{t("Command:")} +[1] {t("Execute")} +[2] {t("Install")} +[3] {t("Question")} +[4] {t("Fork - Open TODO in a new tab")} +[0] {t("Quit")} """ while True: try: @@ -163,16 +160,16 @@ class TODO: # elif status == "3" or status == "install": # print("install") else: - print(t("cmd_not_found")) + print(t("Command not found !")) print(status) # manipuler() def execute_prompt_ia(self): while True: - help_info = f"""{t("command")} -[0] {t("back")} -{t("ia_prompt")}""" + help_info = f"""{t("Command:")} +[0] {t("Back")} +{t("Write your question ")}""" status = click.prompt(help_info) print() if status == "0": @@ -196,22 +193,23 @@ class TODO: 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")} + help_info = f"""{t("Command:")} +[1] {t("Automation - Demonstration of developed features")} +[2] {t("Code - Developer tools")} +[3] {t("Config - Configuration file management")} +[4] {t("Database - Database tools")} +[5] {t("Doc - Documentation search")} +[6] {t("Git - Git tools")} +[7] {t("GPT code - AI assistant tools")} +[8] {t("Language - Change language / Changer la langue")} +[9] {t("Network - Network tools")} +[10] {t("Process - Execution tools")} +[11] {t("Run - Execute and install an instance")} +[12] {t("Security - Dependency security audit")} +[13] {t("Test - Test an Odoo module")} +[14] {t("Update - Update all developed staging source code")} +[15] {t("Deploy - Deploy ERPLibre locally")} +[0] {t("Back")} """ while True: status = click.prompt(help_info) @@ -274,8 +272,12 @@ class TODO: status = self.prompt_execute_update() if status is not False: return + elif status == "15": + status = self.prompt_execute_deploy() + if status is not False: + return else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_install(self): print("Detect first installation from code source.") @@ -355,7 +357,7 @@ class TODO: ), "0": ( "0", - f"0: {t('menu_quit')}", + f"0: {t('Quit')}", ), } commands_end = {} @@ -373,7 +375,7 @@ class TODO: label += " - Installed" if odoo_version == odoo_installed_version: label += " - Actual" - if version_info.get("default"): + if version_info.get("Default"): label += " - Default" if version_info.get("is_deprecated"): label += " - Deprecated" @@ -391,11 +393,13 @@ class TODO: odoo_version_input = "" while odoo_version_input not in install_commands: if odoo_version_input: - print(f"{t('error_value')} '{odoo_version_input}'") + print( + f"{t('Error, cannot understand value')} '{odoo_version_input}'" + ) str_input_dyn_odoo_version = ( - f"💬 {t('choose_version')}\n\t" + f"💬 {t('Choose a version:')}\n\t" + "\n\t".join([a[1] for a in install_commands.values()]) - + f"\n{t('selection')}" + + f"\n{t('Select: ')}" ) odoo_version_input = ( input(str_input_dyn_odoo_version).strip().lower() @@ -405,7 +409,7 @@ class TODO: return cmd_intern = install_commands.get(odoo_version_input)[2] - print(f"{t('will_execute')}\n{cmd_intern}") + 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 @@ -415,7 +419,9 @@ class TODO: cmd_intern, shell=True, executable="/bin/bash", check=True ) except subprocess.CalledProcessError as e: - print(f"{t('script_failed')} {e.returncode}.") + print( + f"{t('The Bash script failed with return code')} {e.returncode}." + ) print("Wait after installation and open projects by terminal.") print("make open_terminal") self.restart_script(str(e)) @@ -459,7 +465,12 @@ class TODO: db_name, extra_cmd_web_login=extra_cmd_web_login ) - command = instance.get("command") + bash_command = instance.get("bash_command") + if bash_command: + print(f"{t('Will execute:')} {bash_command}") + self.execute.exec_command_live(bash_command, source_erplibre=False) + + command = instance.get("Command:") if command: self.prompt_execute_selenium( command=command, extra_cmd_web_login=extra_cmd_web_login @@ -470,8 +481,8 @@ class TODO: callback(instance) def fill_help_info(self, choices): - help_info = t("command") + "\n" - help_end = f"[0] {t('back')}\n" + 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: @@ -492,14 +503,14 @@ class TODO: # Support mobile ERPLibre if os.path.exists(MOBILE_HOME_PATH): menu_entry = { - "prompt_description": t("mobile_compile_run"), + "prompt_description": t("Mobile - Compile and run software"), "callback": self.callback_make_mobile_home, } choices.append(menu_entry) # Support custom database to execute menu_entry = { - "prompt_description": t("choose_database"), + "prompt_description": t("Choose your database"), "callback": self.callback_execute_custom_database, } choices.insert(0, menu_entry) @@ -516,7 +527,9 @@ class TODO: int_cmd = int(status) if 1 < int_cmd <= init_len: cmd_no_found = False - status = click.confirm(t("new_instance_confirm")) + status = click.confirm( + t("Do you want a new instance?") + ) instance = choices[int_cmd - 1] self.execute_from_configuration( instance, @@ -533,7 +546,7 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_function(self): choices = self.config_file.get_config("function") @@ -555,11 +568,11 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_update(self): # self.execute.exec_command_live(f"make {makefile_cmd}") - print(f"🤖 {t('update_dev')}") + print(f"🤖 {t('Development update')}") # 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 @@ -570,11 +583,11 @@ class TODO: choices = self.config_file.get_config("update_from_makefile") menu_entry = { - "prompt_description": t("upgrade_odoo_migration"), + "prompt_description": t("Upgrade Odoo - Migration Database"), } choices.append(menu_entry) poetry_entry = { - "prompt_description": t("upgrade_poetry_dependency"), + "prompt_description": t("Upgrade Poetry - Dependency of Odoo"), } choices.append(poetry_entry) help_info = self.fill_help_info(choices) @@ -600,10 +613,50 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) + + def prompt_execute_deploy(self): + print(f"🤖 {t('Deploy ERPLibre to a local directory!')}") + choices = [ + {"prompt_description": t("Clone ERPLibre locally (git clone)")}, + ] + 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_clone_erplibre() + else: + print(t("Command not found !")) + + def _deploy_clone_erplibre(self): + default_path = os.path.expanduser("~/erplibre") + target_path = ( + input(t("Target directory path (default: ~/erplibre): ")).strip() + or default_path + ) + target_path = os.path.expanduser(target_path) + if os.path.exists(target_path): + print(f"{t('Directory already exists: ')}{target_path}") + return + print(t("Cloning ERPLibre...")) + cmd = ( + "git clone" + " https://github.com/erplibre/erplibre" + f" {target_path}" + ) + print(f"{t('Will execute:')} {cmd}") + try: + self.execute.exec_command_live(cmd, source_erplibre=False) + print(f"{t('ERPLibre cloned successfully to: ')}" f"{target_path}") + except Exception as e: + print(f"{t('Error cloning ERPLibre: ')}{e}") def prompt_execute_code(self): - print(f"🤖 {t('code_need')}") + print(f"🤖 {t('What do you need for development?')}") # help_info = """Commande : # [1] Status Git local et distant # [2] Démarrer le générateur de code @@ -619,18 +672,18 @@ class TODO: choices = self.config_file.get_config("code_from_makefile") menu_entry = { - "prompt_description": t("open_shell"), + "prompt_description": t("Open SHELL"), } choices.append(menu_entry) menu_entry = { - "prompt_description": t("upgrade_module"), + "prompt_description": t("Upgrade Module"), } choices.append(menu_entry) choices.append( { - "prompt_description": t("debug"), + "prompt_description": t("Debug"), } ) @@ -658,12 +711,13 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_git(self): - print(f"🤖 {t('git_manage')}") + print(f"🤖 {t('Git management tools!')}") choices = [ - {"prompt_description": t("git_local_server")}, + {"prompt_description": t("Local git server")}, + {"prompt_description": t("Add a remote to a local repository")}, ] # Append config-driven entries @@ -680,6 +734,8 @@ class TODO: return False elif status == "1": self.prompt_execute_git_local_server() + elif status == "2": + self._git_add_remote() else: cmd_no_found = True try: @@ -691,13 +747,40 @@ class TODO: except ValueError: pass if cmd_no_found: - print(t("cmd_not_found")) + print(t("Command not found !")) + + def _git_add_remote(self): + remote_name = ( + input(t("Remote name (default: localhost): ")).strip() + or "localhost" + ) + remote_url = input( + t("Repository address (e.g.: git://192.168.1.100/my-repo.git): ") + ).strip() + if not remote_url: + print(t("Repository address is required!")) + return + cmd = f"git remote add {remote_name} {remote_url}" + print(f"{t('Will execute:')} {cmd}") + try: + self.execute.exec_command_live(cmd, source_erplibre=False) + print(t("Remote added successfully!")) + except Exception as e: + print(f"{t('Error adding remote: ')}{e}") def prompt_execute_git_local_server(self): - print(f"🤖 {t('git_repo_manage')}") + print(f"🤖 {t('Manage local git repository server!')}") choices = [ - {"prompt_description": t("git_repo_deploy_local")}, - {"prompt_description": t("git_repo_deploy_production")}, + { + "prompt_description": t( + "Deploy a local git server (~/.git-server)" + ) + }, + { + "prompt_description": t( + "Deploy a production git server (/srv/git, root required)" + ) + }, ] help_info = self.fill_help_info(choices) @@ -711,21 +794,25 @@ class TODO: elif status == "2": self._prompt_git_server_actions(production_ready=True) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def _prompt_git_server_actions(self, production_ready=False): mode = ( - t("git_mode_production") + t("Production mode (/srv/git, root required)") if production_ready - else t("git_mode_local") + else t("Local mode (~/.git-server)") ) 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")}, + { + "prompt_description": t( + "Run all (init + remote + push + serve)" + ) + }, + {"prompt_description": t("Init - Create bare repos")}, + {"prompt_description": t("Remote - Add local remotes")}, + {"prompt_description": t("Push - Push to local server")}, + {"prompt_description": t("Serve - Start git daemon")}, ] help_info = self.fill_help_info(choices) @@ -760,10 +847,10 @@ class TODO: action="serve", ) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def _deploy_git_server(self, production_ready=False, action="all"): - print(t("git_repo_deploy_starting")) + print(t("Starting git server deployment...")) cmd = ( "python3 ./script/git/git_local_server.py -v" f" --action {action}" ) @@ -775,9 +862,19 @@ class TODO: ) def prompt_execute_gpt_code(self): - print(f"🤖 {t('gpt_code_manage')}") + print(f"🤖 {t('AI assistant tools for development!')}") choices = [ - {"prompt_description": t("gpt_code_claude_commit")}, + {"prompt_description": t("Configure Claude Code configurations")}, + { + "prompt_description": t( + "Add an automation with Claude in todo.py" + ) + }, + { + "prompt_description": t( + "RTK - CLI proxy to reduce LLM token consumption" + ) + }, ] help_info = self.fill_help_info(choices) @@ -787,56 +884,158 @@ class TODO: if status == "0": return False elif status == "1": - self._setup_claude_commit() + self._prompt_claude_configs() + elif status == "2": + self._claude_add_automation() + elif status == "3": + self.prompt_execute_rtk() else: - print(t("cmd_not_found")) + print(t("Command not found !")) - def _setup_claude_commit(self): + def _prompt_claude_configs(self): + print(f"🤖 {t('Deploy Claude Code commands!')}") + choices = [ + {"prompt_description": t("Commit - OCA/Odoo commit command")}, + { + "prompt_description": t( + "Todo Add Command - Add a command to todo.py menu" + ) + }, + {"prompt_description": t("Show installed custom commands")}, + ] + 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_command( + "commit", + "template_claude_commands_commit.md", + personalize=True, + ) + elif status == "2": + self._setup_claude_command( + "todo_add_command", + "template_claude_commands_todo_add_command.md", + ) + elif status == "3": + self._list_claude_commands() + else: + print(t("Command not found !")) + + def _list_claude_commands(self): + commands_dir = os.path.expanduser("~/.claude/commands") + if not os.path.isdir(commands_dir): + print(t("No custom commands found in ~/.claude/commands/")) + return + files = sorted( + f for f in os.listdir(commands_dir) if f.endswith(".md") + ) + if not files: + print(t("No custom commands found in ~/.claude/commands/")) + return + print(t("Claude Code custom commands:")) + print("-" * 50) + for f in files: + filepath = os.path.join(commands_dir, f) + mtime = os.path.getmtime(filepath) + date_str = datetime.datetime.fromtimestamp(mtime).strftime( + "%Y-%m-%d %H:%M" + ) + name = f[:-3] # remove .md + print(f" /{name:<30} {date_str}") + print("-" * 50) + print(f"{t('Total:')}" f" {len(files)}") + + def _setup_claude_command( + self, command_name, template_filename, personalize=False + ): dest_dir = os.path.expanduser("~/.claude/commands") - dest_file = os.path.join(dest_dir, "commit.md") + dest_file = os.path.join(dest_dir, f"{command_name}.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() + print(f"{t('File already exists: ')}{dest_file}") + overwrite = input( + t("Do you want to overwrite the file? (y/Y): ") + ).strip() + if overwrite not in ("y", "Y"): + print(t("Nothing to do.")) + return template_path = os.path.join( os.path.dirname(__file__), "..", "..", "conf", - "template_claude_commands_commit.md", + template_filename, ) try: with open(template_path) as f: content = f.read() - content = content.replace( - "Your Name ", - f"{name} <{email}>", - ) - content = content.replace( - "Your Name ", - f"{name} ", - ) + if personalize: + name = input(t("Enter your full name: ")).strip() + email = input(t("Enter your email: ")).strip() + content = content.replace( + "Your Name ", + 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")) + print(f"{t('File created successfully: ')}{dest_file}") except Exception as e: - print(f"{t('gpt_code_commit_error')}{e}") + print(f"{t('Error creating file: ')}{e}") + + def _claude_add_automation(self): + description = input(t("Description of the command to add: ")).strip() + if not description: + return + command = input(t("Bash command to execute: ")).strip() + if not command: + return + section = ( + input( + t("Menu section (git/code/config/network/process): ") + ).strip() + or "git" + ) + section_key = f"{section}_from_makefile" + config_path = os.path.join(os.path.dirname(__file__), "todo.json") + try: + with open(config_path) as f: + config = json.load(f) + if section_key not in config: + config[section_key] = [] + config[section_key].append( + { + "prompt_description": description, + "bash_command": command, + } + ) + with open(config_path, "w") as f: + json.dump(config, f, indent=4, ensure_ascii=False) + f.write("\n") + print(t("Automation added successfully in todo.json!")) + except Exception as e: + print(f"{t('Error adding automation: ')}{e}") def prompt_execute_doc(self): - print(f"🤖 {t('doc_search')}") + print(f"🤖 {t('Looking for documentation?')}") 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")}, + {"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 milestone")}, ] help_info = self.fill_help_info(choices) @@ -877,14 +1076,18 @@ class TODO: elif status == "4": print("https://github.com/OCA/maintainer-tools/issues/658") else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_database(self): - print(f"🤖 {t('db_modify')}") + print(f"🤖 {t('Make changes to databases!')}") choices = [ - {"prompt_description": t("download_db_backup")}, - {"prompt_description": t("restore_from_backup")}, - {"prompt_description": t("create_backup")}, + { + "prompt_description": t( + "Download database to create backup (.zip)" + ) + }, + {"prompt_description": t("Restore from backup (.zip)")}, + {"prompt_description": t("Create backup (.zip)")}, ] help_info = self.fill_help_info(choices) @@ -900,13 +1103,13 @@ class TODO: elif status == "3": self.db_manager.create_backup_from_database() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_process(self): - print(f"🤖 {t('process_manage')}") + print(f"🤖 {t('Manage execution processes!')}") choices = [ - {"prompt_description": t("kill_process_port")}, - {"prompt_description": t("kill_git_daemon")}, + {"prompt_description": t("Kill Odoo process from actual port")}, + {"prompt_description": t("Kill git daemon server process")}, ] help_info = self.fill_help_info(choices) @@ -920,23 +1123,139 @@ class TODO: elif status == "2": self.process_kill_git_daemon() else: - print(t("cmd_not_found")) + print(t("Command 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")) + print(t("Git daemon process killed.")) + + def prompt_execute_rtk(self): + print( + f"🤖 {t('Manage RTK (Rust Token Killer) for token optimization!')}" + ) + choices = [ + {"prompt_description": t("Install RTK")}, + {"prompt_description": t("Check RTK version")}, + {"prompt_description": t("Show cumulative token savings")}, + {"prompt_description": t("Discover optimization opportunities")}, + {"prompt_description": t("Initialize global auto-rewrite hook")}, + {"prompt_description": t("Check RTK status")}, + ] + help_info = self.fill_help_info(choices) + + while True: + status = click.prompt(help_info) + print() + if status == "0": + return False + elif status == "1": + self.rtk_install() + elif status == "2": + self.rtk_check_version() + elif status == "3": + self.rtk_show_gain() + elif status == "4": + self.rtk_discover() + elif status == "5": + self.rtk_init_global() + elif status == "6": + self.rtk_check_status() + else: + print(t("Command not found !")) + + def rtk_install(self): + print(f"🤖 {t('Installation method:')}") + choices = [ + {"prompt_description": t("curl - Automatic install script")}, + {"prompt_description": t("brew - Homebrew (macOS/Linux)")}, + { + "prompt_description": t( + "cargo - Build from source (Rust required)" + ) + }, + ] + help_info = self.fill_help_info(choices) + status = click.prompt(help_info) + print() + if status == "0": + return + elif status == "1": + self.execute.exec_command_live( + "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh", + source_erplibre=False, + ) + elif status == "2": + self.execute.exec_command_live( + "brew install rtk", + source_erplibre=False, + ) + elif status == "3": + self.execute.exec_command_live( + "cargo install --git https://github.com/rtk-ai/rtk", + source_erplibre=False, + ) + else: + print(t("Command not found !")) + + def rtk_check_version(self): + self.execute.exec_command_live( + "rtk --version", + source_erplibre=False, + ) + + def rtk_show_gain(self): + self.execute.exec_command_live( + "rtk gain", + source_erplibre=False, + ) + + def rtk_discover(self): + self.execute.exec_command_live( + "rtk discover", + source_erplibre=False, + ) + + def rtk_init_global(self): + self.execute.exec_command_live( + "rtk init --global", + source_erplibre=False, + ) + + def rtk_check_status(self): + rtk_path = shutil.which("rtk") + if rtk_path is None: + print(t("RTK is not installed. Use option 1 to install it.")) + return + + result = self.execute.exec_command_live( + "rtk --version", + source_erplibre=False, + quiet=True, + return_status_and_output=True, + ) + if isinstance(result, tuple) and result[0] == 0: + version_output = " ".join(result[1]).strip() + print(f"{t('RTK is installed, version: ')}{version_output}") + else: + print(f"{t('RTK is installed, version: ')}?") + + config_path = os.path.expanduser("~/.config/rtk/config.toml") + if os.path.exists(config_path): + print(t("Global auto-rewrite hook: active")) + else: + print(t("Global auto-rewrite hook: inactive")) def prompt_execute_config(self): - print(f"🤖 {t('config_manage')}") + print(f"🤖 {t('Manage ERPLibre and Odoo configuration!')}") 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")}, + {"prompt_description": t("Generate all configuration")}, + {"prompt_description": t("Generate from pre-configuration")}, + {"prompt_description": t("Generate from backup file")}, + {"prompt_description": t("Generate from database")}, + {"prompt_description": t("Setup queue job for parallelism")}, ] help_info = self.fill_help_info(choices) @@ -956,15 +1275,15 @@ class TODO: elif status == "5": self.generate_config_queue_job() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_network(self): - print(f"🤖 {t('network_tools')}") + print(f"🤖 {t('Network tools!')}") choices = [ - {"prompt_description": t("ssh_port_forwarding")}, + {"prompt_description": t("SSH port-forwarding")}, { "prompt_description": t( - "network_performance_request_per_second" + "Network performance request per second" ) }, ] @@ -980,7 +1299,7 @@ class TODO: elif status == "2": self.generate_network_performance_test() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def generate_network_port_forwarding(self, add_arg=None): # ssh -L local_port:localhost:remote_port SSH_connection @@ -1007,9 +1326,13 @@ class TODO: ) def prompt_execute_security(self): - print(f"🤖 {t('security_audit')}") + print(f"🤖 {t('Dependency security audit!')}") choices = [ - {"prompt_description": t("pip_audit_desc")}, + { + "prompt_description": t( + "pip-audit - Check vulnerabilities on Python environments" + ) + }, ] help_info = self.fill_help_info(choices) @@ -1021,14 +1344,14 @@ class TODO: elif status == "1": self.execute_pip_audit() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def prompt_execute_test(self): - print(f"🤖 {t('test_description')}") + print(f"🤖 {t('Test an Odoo module on a temporary database!')}") choices = [ - {"prompt_description": t("test_run_module")}, - {"prompt_description": t("test_run_module_coverage")}, - {"prompt_description": t("test_run_unit_tests")}, + {"prompt_description": t("Test a module")}, + {"prompt_description": t("Test a module with code coverage")}, + {"prompt_description": t("ERPLibre unit tests")}, ] help_info = self.fill_help_info(choices) @@ -1044,25 +1367,29 @@ class TODO: elif status == "3": self.execute_unit_tests() else: - print(t("cmd_not_found")) + print(t("Command not found !")) def execute_test_module(self, coverage=False): # Module name - module_name = input(t("test_enter_module_name")).strip() + module_name = input(t("Module name to test: ")).strip() if not module_name: - print(t("test_module_required")) + print(t("Module name is required!")) return # Database name - db_name = input(t("test_db_name")).strip() + db_name = input( + t("Temporary database name (default: test_todo_tmp): ") + ).strip() if not db_name: db_name = "test_todo_tmp" # Extra modules - extra_modules = input(t("test_install_extra_modules")).strip() + extra_modules = input( + t("Extra modules to install (comma-separated, empty for none): ") + ).strip() # Log level - log_level = input(t("test_log_level")).strip() + log_level = input(t("Log level (default: test): ")).strip() if not log_level: log_level = "test" @@ -1072,7 +1399,7 @@ class TODO: modules_to_install += f",{extra_modules}" # Step 1: Create temp DB - print(f"\n--- {t('test_creating_db')} '{db_name}' ---") + print(f"\n--- {t('Creating temporary database')} '{db_name}' ---") cmd_restore = f"./script/database/db_restore.py --database {db_name}" self.execute.exec_command_live( cmd_restore, @@ -1081,9 +1408,7 @@ class TODO: ) # Step 2: Install modules - print( - f"\n--- {t('test_installing_modules')}: {modules_to_install} ---" - ) + print(f"\n--- {t('Installing modules')}: {modules_to_install} ---") cmd_install = ( f"./script/addons/install_addons.sh" f" {db_name} {modules_to_install}" @@ -1095,7 +1420,7 @@ class TODO: ) # Step 3: Run tests - print(f"\n--- {t('test_running')}: {module_name} ---") + print(f"\n--- {t('Running tests')}: {module_name} ---") cmd_test = ( f"ODOO_MODE_TEST=true" f" ./run.sh" @@ -1113,18 +1438,22 @@ class TODO: ) if status_code == 0: - print(f"\n✅ {t('test_success')}") + print(f"\n✅ {t('Tests completed successfully!')}") else: - print(f"\n❌ {t('test_failed')} {status_code}") + print(f"\n❌ {t('Tests failed with return code')} {status_code}") # Step 4: Cleanup lang = get_lang() - keep_input = input(t("test_keep_db")).strip().lower() + keep_input = ( + input(t("Keep the temporary database? (y/N): ")).strip().lower() + ) keep = keep_input in (("o", "oui") if lang == "fr" else ("y", "yes")) if keep: - print(f"{t('test_db_kept')}: {db_name}") + print(f"{t('Database kept')}: {db_name}") else: - print(f"\n--- {t('test_cleaning_db')} '{db_name}' ---") + print( + f"\n--- {t('Cleaning up temporary database')} '{db_name}' ---" + ) cmd_drop = f"./odoo_bin.sh db --drop --database {db_name}" self.execute.exec_command_live( cmd_drop, @@ -1133,7 +1462,7 @@ class TODO: ) def execute_unit_tests(self): - print(f"\n--- {t('test_unit_running')} ---") + print(f"\n--- {t('Running unit tests')} ---") cmd = ( ".venv.erplibre/bin/python -m unittest discover" " -s test -p 'test_*.py' -v" @@ -1144,9 +1473,11 @@ class TODO: return_status_and_output=True, ) if status_code == 0: - print(f"\n✅ {t('test_unit_success')}") + print(f"\n✅ {t('All unit tests passed')}") else: - print(f"\n❌ {t('test_unit_failed')}: {status_code}") + print( + f"\n❌ {t('Some unit tests failed, exit code')}: {status_code}" + ) def execute_pip_audit(self): versions, installed_versions, odoo_installed_version = ( @@ -1169,9 +1500,9 @@ class TODO: 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')}" + label += f" - {t('Current')}" + if version_info.get("Default"): + label += f" - {t('Default')}" environments[key_s] = { "label": label, @@ -1181,20 +1512,26 @@ class TODO: } if not environments: - print(t("no_env_installed")) + print( + t( + "No installed environment found. Install an Odoo version first." + ) + ) return # Show selection menu str_input = ( - f"💬 {t('choose_env_audit')}\n\t" + f"💬 {t('Choose an environment for the audit:')}\n\t" + "\n\t".join([v["label"] for v in environments.values()]) - + f"\n\t0: {t('back')}" - + f"\n{t('selection')}" + + f"\n\t0: {t('Back')}" + + f"\n{t('Select: ')}" ) env_input = "" while env_input not in environments and env_input != "0": if env_input: - print(f"{t('error_value')}" f" '{env_input}'") + print( + f"{t('Error, cannot understand value')}" f" '{env_input}'" + ) env_input = input(str_input).strip() if env_input == "0": @@ -1205,12 +1542,12 @@ class TODO: req_path = selected["req_path"] if not os.path.isfile(req_path): - print(f"{t('dep_file_not_found')}{req_path}") + print(f"{t('Dependencies 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}") + print(f"{t('Execution: ')}{cmd}") self.execute.exec_command_live( cmd, source_erplibre=True, @@ -1237,10 +1574,10 @@ class TODO: 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": t("base")}, + {"prompt_description": t("base + code_generator")}, + {"prompt_description": t("base + image_db")}, + {"prompt_description": t("all")}, # {"prompt_description": "base + migration"}, ] help_info = self.fill_help_info(choices) @@ -1269,11 +1606,11 @@ class TODO: # str_group = f"--group {group}" # self.generate_config(add_arg=str_group) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def debug_ide(self): choices = [ - {"prompt_description": t("debug_todo_py")}, + {"prompt_description": t("Debug todo.py")}, ] help_info = self.fill_help_info(choices) @@ -1288,7 +1625,7 @@ class TODO: os.path.join(os.getcwd(), "script/todo/todo.py"), ) else: - print(t("cmd_not_found")) + print(t("Command not found !")) def generate_config_from_backup(self): file_name = self.db_manager.open_file_image_db() @@ -1385,9 +1722,6 @@ class TODO: # # self.restart_script(e) try: # TODO duplicate - import tkinter as tk - from tkinter import filedialog - import click import humanize import openai @@ -1487,7 +1821,7 @@ class TODO: ) def restart_script(self, last_error): - print(f"🤖 {t('reboot_todo')}") + 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( @@ -1655,13 +1989,13 @@ if __name__ == "__main__": todo.crash_diagnostic(CRASH_E) todo.run() except KeyboardInterrupt: - print(t("keyboard_interrupt")) + 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") + print(f"\n{t('TODO execution time')} {humain_time}\n") else: - print(f"\n{t('execution_time')} {duration_sec:.2f} sec.\n") + print(f"\n{t('TODO execution time')} {duration_sec:.2f} sec.\n") diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index a4f3ca3..1e9846e 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -15,539 +15,732 @@ TRANSLATIONS = { "fr": "L'importation est un succès!", "en": "Importation success!", }, - "opening": { + "Opening TODO ...": { "fr": "Ouverture de TODO en cours ...", "en": "Opening TODO ...", }, - "enter_directives": { + "=> Enter your choice by number and press Enter!": { "fr": "=> Entre tes directives par son chiffre et fait Entrée!", "en": "=> Enter your choice by number and press Enter!", }, - "command": { + "Command:": { "fr": "Commande :", "en": "Command:", }, - "menu_execute": { + "Execute": { "fr": "Exécution", "en": "Execute", }, - "menu_install": { + "Install": { "fr": "Installation", "en": "Install", }, - "menu_question": { + "Question": { "fr": "Question", "en": "Question", }, - "menu_fork": { + "Fork - Open TODO in a new tab": { "fr": "Fork - Ouvre TODO dans une nouvelle tabulation", "en": "Fork - Open TODO in a new tab", }, - "menu_quit": { + "Quit": { "fr": "Quitter", "en": "Quit", }, - "cmd_not_found": { + "Command not found !": { "fr": "Commande non trouvée !", "en": "Command not found !", }, - "back": { + "Back": { "fr": "Retour", "en": "Back", }, # Execute submenu - "menu_run": { + "Run - Execute and install an instance": { "fr": "Run - Exécuter et installer une instance", "en": "Run - Execute and install an instance", }, - "menu_automation": { + "Automation - Demonstration of developed features": { "fr": "Automatisation - Demonstration des fonctions développées", "en": "Automation - Demonstration of developed features", }, - "menu_update": { + "Update - Update all developed staging source code": { "fr": "Mise à jour - Update all developed staging source code", "en": "Update - Update all developed staging source code", }, - "menu_code": { + "Code - Developer tools": { "fr": "Code - Outil pour développeur", "en": "Code - Developer tools", }, - "menu_doc": { + "Doc - Documentation search": { "fr": "Doc - Recherche de documentation", "en": "Doc - Documentation search", }, - "menu_database": { + "Database - Database tools": { "fr": "Database - Outils sur les bases de données", "en": "Database - Database tools", }, - "menu_process": { + "Process - Execution tools": { "fr": "Process - Outils sur les executions", "en": "Process - Execution tools", }, - "menu_config": { + "Config - Configuration file management": { "fr": "Config - Traitement du fichier de configuration", "en": "Config - Configuration file management", }, - "menu_network": { + "Network - Network tools": { "fr": "Réseau - Outil réseautique", "en": "Network - Network tools", }, - "menu_security": { + "Security - Dependency security audit": { "fr": "Sécurité - Audit de sécurité des dépendances", "en": "Security - Dependency security audit", }, - "menu_lang": { + "RTK - CLI proxy to reduce LLM token consumption": { + "fr": "RTK - Proxy CLI pour réduire la consommation de tokens LLM", + "en": "RTK - CLI proxy to reduce LLM token consumption", + }, + "Language - Change language / Changer la langue": { "fr": "Langue - Changer la langue / Change language", "en": "Language - Change language / Changer la langue", }, + # Deploy section + "Deploy - Deploy ERPLibre locally": { + "fr": "Déploiement - Déployer ERPLibre localement", + "en": "Deploy - Deploy ERPLibre locally", + }, + "Deploy ERPLibre to a local directory!": { + "fr": "Déployer ERPLibre dans un répertoire local!", + "en": "Deploy ERPLibre to a local directory!", + }, + "Clone ERPLibre locally (git clone)": { + "fr": "Cloner ERPLibre localement (git clone)", + "en": "Clone ERPLibre locally (git clone)", + }, + "Target directory path (default: ~/erplibre): ": { + "fr": "Chemin du répertoire cible (défaut: ~/erplibre) : ", + "en": "Target directory path (default: ~/erplibre): ", + }, + "Directory already exists: ": { + "fr": "Le répertoire existe déjà : ", + "en": "Directory already exists: ", + }, + "Cloning ERPLibre...": { + "fr": "Clonage d'ERPLibre en cours...", + "en": "Cloning ERPLibre...", + }, + "ERPLibre cloned successfully to: ": { + "fr": "ERPLibre cloné avec succès dans : ", + "en": "ERPLibre cloned successfully to: ", + }, + "Error cloning ERPLibre: ": { + "fr": "Erreur lors du clonage d'ERPLibre : ", + "en": "Error cloning ERPLibre: ", + }, + # RTK (Rust Token Killer) + "Manage RTK (Rust Token Killer) for token optimization!": { + "fr": "Gérer RTK (Rust Token Killer) pour optimiser les tokens!", + "en": "Manage RTK (Rust Token Killer) for token optimization!", + }, + "Install RTK": { + "fr": "Installer RTK", + "en": "Install RTK", + }, + "Check RTK version": { + "fr": "Vérifier la version de RTK", + "en": "Check RTK version", + }, + "Show cumulative token savings": { + "fr": "Afficher les économies de tokens cumulées", + "en": "Show cumulative token savings", + }, + "Discover optimization opportunities": { + "fr": "Identifier les opportunités d'optimisation", + "en": "Discover optimization opportunities", + }, + "Initialize global auto-rewrite hook": { + "fr": "Initialiser le hook auto-rewrite global", + "en": "Initialize global auto-rewrite hook", + }, + "Check RTK status": { + "fr": "Vérifier le statut de RTK", + "en": "Check RTK status", + }, + "RTK is not installed. Use option 1 to install it.": { + "fr": "RTK n'est pas installé. Utilisez l'option 1 pour l'installer.", + "en": "RTK is not installed. Use option 1 to install it.", + }, + "RTK is installed, version: ": { + "fr": "RTK est installé, version : ", + "en": "RTK is installed, version: ", + }, + "Global auto-rewrite hook: active": { + "fr": "Hook auto-rewrite global : actif", + "en": "Global auto-rewrite hook: active", + }, + "Global auto-rewrite hook: inactive": { + "fr": "Hook auto-rewrite global : inactif", + "en": "Global auto-rewrite hook: inactive", + }, + "Installation method:": { + "fr": "Méthode d'installation :", + "en": "Installation method:", + }, + "curl - Automatic install script": { + "fr": "curl - Script d'installation automatique", + "en": "curl - Automatic install script", + }, + "brew - Homebrew (macOS/Linux)": { + "fr": "brew - Homebrew (macOS/Linux)", + "en": "brew - Homebrew (macOS/Linux)", + }, + "cargo - Build from source (Rust required)": { + "fr": "cargo - Compilation depuis les sources (Rust requis)", + "en": "cargo - Build from source (Rust required)", + }, # Prompts and messages - "enter_password": { + "Enter your password: ": { "fr": "Entrez votre mot de passe : ", "en": "Enter your password: ", }, - "ia_prompt": { + "Write your question ": { "fr": "Écrit moi ta question ", "en": "Write your question ", }, - "new_instance_confirm": { + "Do you want a new instance?": { "fr": "Voulez-vous une nouvelle instance?", "en": "Do you want a new instance?", }, - "ssh_port_forwarding": { + "SSH port-forwarding": { "fr": "SSH port-forwarding", "en": "SSH port-forwarding", }, - "network_performance_request_per_second": { + "Network performance request per second": { "fr": "Performance réseau en requêtes par seconde", "en": "Network performance request per second", }, - "setup_queue_job_for_parallelism": { + "Setup queue job for parallelism": { "fr": "Configurer la file d'attente pour l'exécution parallèle", "en": "Setup queue job for parallelism", }, - "choose_database": { + "Choose your database": { "fr": "Choisir sa base de données", "en": "Choose your database", }, - "update_dev": { + "Development update": { "fr": "Mise à jour du développement", "en": "Development update", }, - "code_need": { + "What do you need for development?": { "fr": "Qu'avez-vous de besoin pour développer?", "en": "What do you need for development?", }, - "doc_search": { + "Looking for documentation?": { "fr": "Vous cherchez de la documentation?", "en": "Looking for documentation?", }, - "db_modify": { + "Make changes to databases!": { "fr": "Faites des modifications sur les bases de données!", "en": "Make changes to databases!", }, - "process_manage": { + "Manage execution processes!": { "fr": "Manipuler les processus d'exécution!", "en": "Manage execution processes!", }, - "config_manage": { + "Manage ERPLibre and Odoo configuration!": { "fr": "Manipuler la configuration ERPLibre et Odoo!", "en": "Manage ERPLibre and Odoo configuration!", }, - "network_tools": { + "Network tools!": { "fr": "Outil réseautique!", "en": "Network tools!", }, - "security_audit": { + "Dependency security audit!": { "fr": "Audit de securite des dépendances!", "en": "Dependency security audit!", }, - "script_failed": { + "The Bash script failed with return code": { "fr": "Le script Bash a échoué avec le code de retour", "en": "The Bash script failed with return code", }, - "no_env_installed": { + "No installed environment found. Install an Odoo version first.": { "fr": "Aucun environnement installe trouve. Installez d'abord une version d'Odoo.", "en": "No installed environment found. Install an Odoo version first.", }, - "choose_env_audit": { + "Choose an environment for the audit:": { "fr": "Choisir un environnement pour l'audit :", "en": "Choose an environment for the audit:", }, - "selection": { + "Select: ": { "fr": "Sélection : ", "en": "Select: ", }, - "error_value": { + "Error, cannot understand value": { "fr": "Erreur, impossible de comprendre la valeur", "en": "Error, cannot understand value", }, - "dep_file_not_found": { + "Dependencies file not found: ": { "fr": "Fichier de dépendances introuvable : ", "en": "Dependencies file not found: ", }, - "execution": { + "Execution: ": { "fr": "Execution : ", "en": "Execution: ", }, - "current": { + "Current": { "fr": "Actuel", "en": "Current", }, - "default": { + "Default": { "fr": "Défaut", "en": "Default", }, - "reboot_todo": { + "Reboot TODO ...": { "fr": "Reboot TODO ...", "en": "Reboot TODO ...", }, - "pip_audit_desc": { + "pip-audit - Check vulnerabilities on Python environments": { "fr": "pip-audit - Verifier les vulnérabilités sur les environnements Python", "en": "pip-audit - Check vulnerabilities on Python environments", }, - "will_execute": { + "Will execute:": { "fr": "Va exécuter :", "en": "Will execute:", }, - "choose_version": { + "Choose a version:": { "fr": "Choisir une version :", "en": "Choose a version:", }, - "error_cannot_understand": { - "fr": "Erreur, impossible de comprendre la valeur", - "en": "Error, cannot understand value", - }, # todo.json translatable prompt_descriptions - "json_instance_test": { + "Test - Minimal base instance": { "fr": "Test - Instance de base minimale", "en": "Test - Minimal base instance", }, - "json_robot_minimal": { + "Open RobotLibre 🤖 minimal": { "fr": "Ouvrir RobotLibre 🤖 minimal", "en": "Open RobotLibre 🤖 minimal", }, - "json_robot_search": { + "Open RobotLibre 🤖 with search enabled": { "fr": "Ouvrir RobotLibre 🤖 en activant la recherche", "en": "Open RobotLibre 🤖 with search enabled", }, - "json_open_erplibre_todo": { + "Open ERPLibre with TODO 🤖": { "fr": "Ouvrir ERPLibre avec TODO 🤖", "en": "Open ERPLibre with TODO 🤖", }, - "json_update_erplibre_base_test": { + "Update all erplibre_base on database test": { "fr": "Mise à jour de tous les erplibre_base sur la base de données test", "en": "Update all erplibre_base on database test", }, - "json_show_code_status": { + "Show code status": { "fr": "Afficher le statut du code", "en": "Show code status", }, - "json_stash_all_code": { + "Stash all code": { "fr": "Remiser tout le code", "en": "Stash all code", }, - "json_format_modified_code": { + "Format modified code": { "fr": "Formater le code modifié", "en": "Format modified code", }, # todo.py hardcoded prompt_descriptions - "mobile_compile_run": { + "Mobile - Compile and run software": { "fr": "Mobile - Compiler et exécuter le logiciel", "en": "Mobile - Compile and run software", }, - "upgrade_odoo_migration": { + "Upgrade Odoo - Migration Database": { "fr": "Mise à jour Odoo - Migration de base de données", "en": "Upgrade Odoo - Migration Database", }, - "upgrade_poetry_dependency": { + "Upgrade Poetry - Dependency of Odoo": { "fr": "Mise à jour Poetry - Dépendances d'Odoo", "en": "Upgrade Poetry - Dependency of Odoo", }, - "open_shell": { + "Open SHELL": { "fr": "Ouvrir le SHELL", "en": "Open SHELL", }, - "upgrade_module": { + "Upgrade Module": { "fr": "Mise à jour de module", "en": "Upgrade Module", }, - "debug": { + "Debug": { "fr": "Débogage", "en": "Debug", }, - "migration_module_coverage": { + "Migration module coverage": { "fr": "Couverture de migration des modules", "en": "Migration module coverage", }, - "what_change_between_version": { + "What change between version": { "fr": "Quels changements entre les versions", "en": "What change between version", }, - "oca_guidelines": { + "OCA guidelines": { "fr": "Directives OCA", "en": "OCA guidelines", }, - "oca_migration_odoo_19": { + "OCA migration Odoo 19 milestone": { "fr": "Migration OCA Odoo 19 - Jalons", "en": "OCA migration Odoo 19 milestone", }, - "download_db_backup": { + "Download database to create backup (.zip)": { "fr": "Télécharger une base de données pour créer une sauvegarde (.zip)", "en": "Download database to create backup (.zip)", }, - "restore_from_backup": { + "Restore from backup (.zip)": { "fr": "Restaurer a partir d'une sauvegarde (.zip)", "en": "Restore from backup (.zip)", }, - "create_backup": { + "Create backup (.zip)": { "fr": "Créer une sauvegarde (.zip)", "en": "Create backup (.zip)", }, - "kill_process_port": { + "Kill Odoo process from actual port": { "fr": "Terminer le processus Odoo du port actuel", "en": "Kill Odoo process from actual port", }, - "kill_git_daemon": { + "Kill git daemon server process": { "fr": "Terminer le processus du serveur git daemon", "en": "Kill git daemon server process", }, - "kill_git_daemon_done": { + "Git daemon process killed.": { "fr": "Processus git daemon terminé.", "en": "Git daemon process killed.", }, - "generate_all_config": { + "Generate all configuration": { "fr": "Générer toute la configuration", "en": "Generate all configuration", }, - "generate_from_preconfig": { + "Generate from pre-configuration": { "fr": "Générer a partir de la pre-configuration", "en": "Generate from pre-configuration", }, - "generate_from_backup": { + "Generate from backup file": { "fr": "Générer a partir d'un fichier de sauvegarde", "en": "Generate from backup file", }, - "generate_from_database": { + "Generate from database": { "fr": "Générer a partir de la base de données", "en": "Generate from database", }, - "preconfig_base": { + "base": { "fr": "base", "en": "base", }, - "preconfig_base_code_generator": { + "base + code_generator": { "fr": "base + code_generator", "en": "base + code_generator", }, - "preconfig_base_image_db": { + "base + image_db": { "fr": "base + image_db", "en": "base + image_db", }, - "preconfig_all": { + "all": { "fr": "tout", "en": "all", }, - "debug_todo_py": { + "Debug todo.py": { "fr": "Débogage todo.py", "en": "Debug todo.py", }, # Test section - "menu_test": { + "Test - Test an Odoo module": { "fr": "Test - Tester un module Odoo", "en": "Test - Test an Odoo module", }, - "test_description": { + "Test an Odoo module on a temporary database!": { "fr": "Tester un module Odoo sur une base de données temporaire!", "en": "Test an Odoo module on a temporary database!", }, - "test_run_module": { + "Test a module": { "fr": "Tester un module", "en": "Test a module", }, - "test_run_module_coverage": { + "Test a module with code coverage": { "fr": "Tester un module avec couverture de code", "en": "Test a module with code coverage", }, - "test_run_unit_tests": { + "ERPLibre unit tests": { "fr": "Tests unitaires ERPLibre", "en": "ERPLibre unit tests", }, - "test_unit_running": { + "Running unit tests": { "fr": "Exécution des tests unitaires", "en": "Running unit tests", }, - "test_unit_success": { + "All unit tests passed": { "fr": "Tous les tests unitaires ont réussi", "en": "All unit tests passed", }, - "test_unit_failed": { + "Some unit tests failed, exit code": { "fr": "Des tests unitaires ont échoué, code de sortie", "en": "Some unit tests failed, exit code", }, - "test_enter_module_name": { + "Module name to test: ": { "fr": "Nom du module à tester : ", "en": "Module name to test: ", }, - "test_db_name": { + "Temporary database name (default: test_todo_tmp): ": { "fr": "Nom de la base de données temporaire (défaut: test_todo_tmp) : ", "en": "Temporary database name (default: test_todo_tmp): ", }, - "test_install_extra_modules": { + "Extra modules to install (comma-separated, empty for none): ": { "fr": "Modules supplémentaires à installer (séparés par des virgules, vide pour aucun) : ", "en": "Extra modules to install (comma-separated, empty for none): ", }, - "test_log_level": { + "Log level (default: test): ": { "fr": "Niveau de log (défaut: test) : ", "en": "Log level (default: test): ", }, - "test_creating_db": { + "Creating temporary database": { "fr": "Création de la base de données temporaire", "en": "Creating temporary database", }, - "test_installing_modules": { + "Installing modules": { "fr": "Installation des modules", "en": "Installing modules", }, - "test_running": { + "Running tests": { "fr": "Exécution des tests", "en": "Running tests", }, - "test_cleaning_db": { + "Cleaning up temporary database": { "fr": "Suppression de la base de données temporaire", "en": "Cleaning up temporary database", }, - "test_keep_db": { + "Keep the temporary database? (y/N): ": { "fr": "Conserver la base de données temporaire? (o/N) : ", "en": "Keep the temporary database? (y/N): ", }, - "test_db_kept": { + "Database kept": { "fr": "Base de données conservée", "en": "Database kept", }, - "test_success": { + "Tests completed successfully!": { "fr": "Tests terminés avec succès!", "en": "Tests completed successfully!", }, - "test_failed": { + "Tests failed with return code": { "fr": "Les tests ont échoué avec le code de retour", "en": "Tests failed with return code", }, - "test_module_required": { + "Module name is required!": { "fr": "Le nom du module est requis!", "en": "Module name is required!", }, # Git section - "menu_git": { + "Git - Git tools": { "fr": "Git - Outils Git", "en": "Git - Git tools", }, - "git_manage": { + "Git management tools!": { "fr": "Outils de gestion Git!", "en": "Git management tools!", }, - "git_local_server": { + "Local git server": { "fr": "Serveur git local", "en": "Local git server", }, - "git_repo_manage": { + "Manage local git repository server!": { "fr": "Gérer le serveur de dépôts git local!", "en": "Manage local git repository server!", }, - "git_repo_deploy_local": { + "Deploy a local git server (~/.git-server)": { "fr": "Déployer un serveur git local (~/.git-server)", "en": "Deploy a local git server (~/.git-server)", }, - "git_repo_deploy_production": { + "Deploy a production git server (/srv/git, root required)": { "fr": "Déployer un serveur git production (/srv/git, root requis)", "en": "Deploy a production git server (/srv/git, root required)", }, - "git_repo_deploy_starting": { + "Starting git server deployment...": { "fr": "Démarrage du déploiement du serveur git...", "en": "Starting git server deployment...", }, - "git_mode_local": { + "Local mode (~/.git-server)": { "fr": "Mode local (~/.git-server)", "en": "Local mode (~/.git-server)", }, - "git_mode_production": { + "Production mode (/srv/git, root required)": { "fr": "Mode production (/srv/git, root requis)", "en": "Production mode (/srv/git, root required)", }, - "git_action_all": { + "Run all (init + remote + push + serve)": { "fr": "Tout exécuter (init + remote + push + serve)", "en": "Run all (init + remote + push + serve)", }, - "git_action_init": { + "Init - Create bare repos": { "fr": "Init - Créer les bare repos", "en": "Init - Create bare repos", }, - "git_action_remote": { + "Remote - Add local remotes": { "fr": "Remote - Ajouter les remotes locaux", "en": "Remote - Add local remotes", }, - "git_action_push": { + "Push - Push to local server": { "fr": "Push - Pousser vers le serveur local", "en": "Push - Push to local server", }, - "git_action_serve": { + "Serve - Start git daemon": { "fr": "Serve - Démarrer le daemon git", "en": "Serve - Start git daemon", }, + # Git remote add + "Add a remote to a local repository": { + "fr": "Ajouter un remote vers un dépôt local", + "en": "Add a remote to a local repository", + }, + "Remote name (default: localhost): ": { + "fr": "Nom du remote (défaut: localhost) : ", + "en": "Remote name (default: localhost): ", + }, + "Repository address (e.g.: git://192.168.1.100/my-repo.git): ": { + "fr": "Adresse du dépôt (ex: git://192.168.1.100/mon-repo.git) : ", + "en": "Repository address (e.g.: git://192.168.1.100/my-repo.git): ", + }, + "Repository address is required!": { + "fr": "L'adresse du dépôt est requise!", + "en": "Repository address is required!", + }, + "Remote added successfully!": { + "fr": "Remote ajouté avec succès!", + "en": "Remote added successfully!", + }, + "Error adding remote: ": { + "fr": "Erreur lors de l'ajout du remote : ", + "en": "Error adding remote: ", + }, + # Git config vim + "Configure git local editor to vim": { + "fr": "Configuration git local par vim", + "en": "Configure git local editor to vim", + }, + "Git editor configured to vim successfully!": { + "fr": "Éditeur git configuré sur vim avec succès!", + "en": "Git editor configured to vim successfully!", + }, + "Error during configuration: ": { + "fr": "Erreur lors de la configuration : ", + "en": "Error during configuration: ", + }, + # GPT code - Claude automation + "Add an automation with Claude in todo.py": { + "fr": "Ajouter une automatisation avec Claude dans todo.py", + "en": "Add an automation with Claude in todo.py", + }, + "Description of the command to add: ": { + "fr": "Description de la commande à ajouter : ", + "en": "Description of the command to add: ", + }, + "Bash command to execute: ": { + "fr": "Commande bash à exécuter : ", + "en": "Bash command to execute: ", + }, + "Menu section (git/code/config/network/process): ": { + "fr": "Section du menu (git/code/config/network/process) : ", + "en": "Menu section (git/code/config/network/process): ", + }, + "Automation added successfully in todo.json!": { + "fr": "Automatisation ajoutée avec succès dans todo.json!", + "en": "Automation added successfully in todo.json!", + }, + "Error adding automation: ": { + "fr": "Erreur lors de l'ajout de l'automatisation : ", + "en": "Error adding automation: ", + }, # Language selection - "lang_prompt": { + "Choose language / Choisir la langue": { "fr": "Choisir la langue / Choose language", "en": "Choose language / Choisir la langue", }, - "lang_french": { + "French": { "fr": "Francais", "en": "French", }, - "lang_english": { + "English": { "fr": "Anglais", "en": "English", }, - "lang_changed": { + "Language changed to: English": { "fr": "Langue changée pour : Francais", "en": "Language changed to: English", }, - "execution_time": { + "TODO execution time": { "fr": "Temps d'exécution TODO", "en": "TODO execution time", }, - "keyboard_interrupt": { + "Keyboard interrupt": { "fr": "Interruption clavier", "en": "Keyboard interrupt", }, # GPT code section - "menu_gpt_code": { + "GPT code - AI assistant tools": { "fr": "GPT code - Outils d'assistant IA", "en": "GPT code - AI assistant tools", }, - "gpt_code_manage": { + "AI assistant tools for development!": { "fr": "Outils d'assistant IA pour le développement!", "en": "AI assistant tools for development!", }, - "gpt_code_claude_commit": { - "fr": "Configurer le commit Claude Code", - "en": "Configure Claude Code commit", + "Configure Claude Code configurations": { + "fr": "Configurer les configurations Claude Code", + "en": "Configure Claude Code configurations", }, - "gpt_code_enter_name": { + "Commit - OCA/Odoo commit command": { + "fr": "Commit - Commande de commit OCA/Odoo", + "en": "Commit - OCA/Odoo commit command", + }, + "Todo Add Command - Add a command to todo.py menu": { + "fr": "Todo Add Command - Ajouter une commande au menu todo.py", + "en": "Todo Add Command - Add a command to todo.py menu", + }, + "Enter your full name: ": { "fr": "Entrez votre nom complet : ", "en": "Enter your full name: ", }, - "gpt_code_enter_email": { + "Enter your email: ": { "fr": "Entrez votre courriel : ", "en": "Enter your email: ", }, - "gpt_code_commit_exists": { - "fr": "Le fichier ~/.claude/commands/commit.md existe déjà. Aucune action effectuée.", - "en": "File ~/.claude/commands/commit.md already exists. No action taken.", + "Deploy Claude Code commands!": { + "fr": "Déployer les commandes Claude Code!", + "en": "Deploy Claude Code commands!", }, - "gpt_code_commit_created": { - "fr": "Fichier ~/.claude/commands/commit.md créé avec succès!", - "en": "File ~/.claude/commands/commit.md created successfully!", + "Show installed custom commands": { + "fr": "Afficher les commandes personnalisées installées", + "en": "Show installed custom commands", }, - "gpt_code_commit_error": { + "No custom commands found in ~/.claude/commands/": { + "fr": "Aucune commande personnalisée trouvée dans ~/.claude/commands/", + "en": "No custom commands found in ~/.claude/commands/", + }, + "Claude Code custom commands:": { + "fr": "Commandes personnalisées Claude Code :", + "en": "Claude Code custom commands:", + }, + "Total:": { + "fr": "Total :", + "en": "Total:", + }, + "File already exists: ": { + "fr": "Le fichier existe déjà : ", + "en": "File already exists: ", + }, + "Do you want to overwrite the file? (y/Y): ": { + "fr": "Voulez-vous écraser le fichier? (y/Y) : ", + "en": "Do you want to overwrite the file? (y/Y): ", + }, + "Nothing to do.": { + "fr": "Rien à faire.", + "en": "Nothing to do.", + }, + "File created successfully: ": { + "fr": "Fichier créé avec succès : ", + "en": "File created successfully: ", + }, + "Error creating file: ": { "fr": "Erreur lors de la création du fichier : ", "en": "Error creating file: ", },