Compare commits
2 commits
32d25b5344
...
03ed3a6a87
| Author | SHA1 | Date | |
|---|---|---|---|
| 03ed3a6a87 | |||
| f2da91b0fe |
11 changed files with 570 additions and 158 deletions
|
|
@ -1,14 +1,10 @@
|
|||
# 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é |
|
||||
Odoo 12.0 à 18.0, **18.0 par défaut**. Les 12 à 15 sont dépréciées.
|
||||
|
||||
Configuration dans `conf/supported_version_erplibre.json`.
|
||||
Fichiers de version : `.odoo-version`, `.erplibre-version`, `.poetry-version`, `.python-odoo-version`.
|
||||
La correspondance Odoo ↔ Python ↔ Poetry fait autorité dans
|
||||
`conf/supported_version_erplibre.json` (ses clés portent déjà le couple, ex.
|
||||
`odoo18.0_python3.12.10`) — la lire plutôt que de mémoriser un tableau.
|
||||
|
||||
Version active du checkout : `.odoo-version`, `.erplibre-version`,
|
||||
`.poetry-version`, `.python-odoo-version`.
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
# 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é)
|
||||
```
|
||||
|
|
@ -1,21 +1,12 @@
|
|||
# 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)
|
||||
Le formatage et le lint sont entièrement décrits par les fichiers de
|
||||
configuration du dépôt — les lire plutôt que de supposer : `.flake8`,
|
||||
`.editorconfig`, et les sections `[tool.black]` / `[tool.isort]` de
|
||||
`pyproject.toml`. `make format` applique l'ensemble.
|
||||
|
||||
## 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
|
||||
Prettier (via npm) formate XML/JSON/YAML ; `.editorconfig` donne les
|
||||
indentations par type de fichier.
|
||||
|
||||
## Git
|
||||
- Branches : `develop` (développement), `master` (production)
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
# 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`
|
||||
|
|
@ -1,55 +1,11 @@
|
|||
# Documentation multilingue
|
||||
|
||||
La documentation est bilingue (anglais/français) via **mmg** (Multilingual Markdown Generator).
|
||||
La documentation est bilingue via **mmg** : les sources sont les `.base.md`,
|
||||
qui génèrent `FICHIER.md` (anglais) et `FICHIER.fr.md` (français).
|
||||
|
||||
## 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 : `<!-- [en] -->`, `<!-- [fr] -->`, `<!-- [common] -->` (blocs de code partagés)
|
||||
- **Ne jamais modifier directement** un `.md` ou `.fr.md` généré : la
|
||||
modification est perdue au prochain `make doc_markdown`. Éditer le
|
||||
`.base.md` correspondant.
|
||||
|
||||
## 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 `<!-- [common] -->`, le texte dans `<!-- [en] -->` et `<!-- [fr] -->`
|
||||
- En-tête obligatoire dans chaque `.base.md` :
|
||||
```
|
||||
<!---------------------------->
|
||||
<!-- multilingual suffix: en, fr -->
|
||||
<!-- no suffix: en -->
|
||||
<!---------------------------->
|
||||
```
|
||||
|
||||
## 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`
|
||||
Le mode d'emploi complet (marqueurs, en-tête obligatoire, i18n du CLI TODO)
|
||||
est dans la skill `erplibre-doc-i18n`.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
# Commandes essentielles
|
||||
---
|
||||
name: erplibre-commands
|
||||
description: Commandes ERPLibre — changer de version Odoo, lancer/tester une instance, opérations base de données, tester un module précis avec couverture, formatage, Docker, Google Repo, installation. À charger dès qu'une commande make ou un script du dépôt est nécessaire.
|
||||
---
|
||||
|
||||
|
||||
## Changement de version Odoo
|
||||
```bash
|
||||
60
.claude/skills/erplibre-doc-i18n/SKILL.md
Normal file
60
.claude/skills/erplibre-doc-i18n/SKILL.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
name: erplibre-doc-i18n
|
||||
description: Documentation bilingue ERPLibre (mmg, fichiers .base.md) et internationalisation du CLI TODO (todo_i18n.py, clés TRANSLATIONS). À charger pour rédiger ou régénérer de la documentation, ou pour ajouter une chaîne traduisible au CLI.
|
||||
---
|
||||
|
||||
# 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 : `<!-- [en] -->`, `<!-- [fr] -->`, `<!-- [common] -->` (blocs de code partagés)
|
||||
|
||||
## Commandes
|
||||
```bash
|
||||
make doc_markdown # Regénérer toute la doc multilingue
|
||||
```
|
||||
|
||||
## Convention
|
||||
- Toujours modifier le fichier `.base.md` correspondant, puis exécuter `make doc_markdown`
|
||||
- Les blocs de code vont dans `<!-- [common] -->`, le texte dans `<!-- [en] -->` et `<!-- [fr] -->`
|
||||
- En-tête obligatoire dans chaque `.base.md` :
|
||||
```
|
||||
<!---------------------------->
|
||||
<!-- multilingual suffix: en, fr -->
|
||||
<!-- no suffix: en -->
|
||||
<!---------------------------->
|
||||
```
|
||||
|
||||
> Les fichiers concernés se listent par `find . -name '*.base.md'`.
|
||||
|
||||
## 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. `EL_LANG` dans `env_var.sh` (lu en premier, défaut `"fr"`)
|
||||
2. Variable d'environnement `EL_LANG`
|
||||
3. 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 TODO > Configuration > Langue
|
||||
|
||||
### 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`
|
||||
|
||||
> La clé EST la chaîne anglaise. Vérifier l'absence de doublon dans
|
||||
> `TRANSLATIONS` avant d'ajouter : une clé en double écrase silencieusement
|
||||
> la précédente.
|
||||
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -14,9 +14,11 @@ Version Odoo par défaut : **18.0** (support officiel ERPLibre 1.6.0)
|
|||
|
||||
- Toujours vérifier la version Odoo active avant de modifier du code (`cat .odoo-version`)
|
||||
- Les addons sont dans `addons/` et gérés par Google Repo — ne pas modifier la structure des dépôts
|
||||
- Utiliser le venv approprié : `.venv.odoo{XX}/bin/python` pour le code Odoo
|
||||
- Utiliser le venv approprié pour le code Odoo. Son nom porte les DEUX versions
|
||||
(`.venv.odoo18.0_python3.12.10/bin/python`) : le retrouver par
|
||||
`ls -d .venv.odoo*` plutôt que de le composer de tête
|
||||
- Les scripts ERPLibre utilisent `.venv.erplibre/bin/python`
|
||||
- Le Makefile principal inclut 12 fragments depuis `conf/make.*.Makefile`
|
||||
- Le Makefile principal inclut des fragments depuis `conf/make.*.Makefile`
|
||||
- Les fichiers privés vont dans `private/` (non versionné)
|
||||
- La DB PostgreSQL par défaut est sur le port 5432, mot de passe admin : `admin`
|
||||
- Port Odoo par défaut : 8069, longpolling : 8072
|
||||
|
|
@ -36,12 +38,19 @@ 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 |
|
||||
| `01-versions.md` | Versions Odoo supportées, où lit-on la correspondance |
|
||||
| `04-code-conventions.md` | Où sont les configs de format, conventions Git |
|
||||
| `06-code-generator.md` | Génération de modules Odoo |
|
||||
| `07-documentation.md` | Documentation multilingue (mmg) + i18n CLI |
|
||||
| `07-documentation.md` | Interdit : ne pas éditer les `.md` générés |
|
||||
| `08-deployment.md` | Docker, systemd, nginx, SSL, DNS |
|
||||
| `09-workflow.md` | Workflow orchestration + task management |
|
||||
|
||||
Chargées à la demande (`.claude/skills/`) :
|
||||
|
||||
| Skill | Contenu |
|
||||
|-------|---------|
|
||||
| `erplibre-commands` | Commandes make et scripts : versions, run, tests, DB, Docker, repo |
|
||||
| `erplibre-doc-i18n` | Mode d'emploi mmg (`.base.md`) et i18n du CLI TODO |
|
||||
|
||||
L'arborescence et la liste des venvs ne sont plus documentées : `ls` et
|
||||
`ls -d .venv.*` en donnent l'état réel, la doc dérivait de la réalité.
|
||||
|
|
|
|||
201
script/todo/migration_stats.py
Normal file
201
script/todo/migration_stats.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
"""Statistiques d'une migration Odoo, en lecture seule.
|
||||
|
||||
Ne touche NI la base NI le fichier de progression : tout se déduit du journal
|
||||
de migration et des traces laissées sous `private/odoo/migration/<base>/`.
|
||||
C'est ce qui permet de consulter l'état d'une migration en cours depuis une
|
||||
autre session sans risquer de la perturber.
|
||||
|
||||
`compute()` reçoit le contexte déjà construit par
|
||||
`TodoUpgrade.resume_context()` — étapes et montées de version — pour ne pas
|
||||
réimplémenter une seconde fois la lecture des clés « state_* », qui
|
||||
divergerait de l'écran de reprise.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
|
||||
try:
|
||||
from script.todo.todo_i18n import t
|
||||
except Exception: # pragma: no cover - repli si i18n indisponible
|
||||
|
||||
def t(key: str) -> str:
|
||||
return key
|
||||
|
||||
|
||||
def _as_int(value):
|
||||
"""Clé de version en entier. JSON transforme les clés numériques en
|
||||
chaînes : « 13 » et 13 désignent la même version."""
|
||||
try:
|
||||
return int(float(value))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def fmt_delay(start, end):
|
||||
"""Écart lisible entre deux horodatages « str(datetime) »."""
|
||||
try:
|
||||
a = datetime.datetime.fromisoformat(str(start))
|
||||
b = datetime.datetime.fromisoformat(str(end))
|
||||
except (TypeError, ValueError):
|
||||
return "?"
|
||||
secs = int(abs((b - a).total_seconds()))
|
||||
days, rest = divmod(secs, 86400)
|
||||
hours, rest = divmod(rest, 3600)
|
||||
minutes = rest // 60
|
||||
if days:
|
||||
return f"{days} j {hours:02d} h"
|
||||
if hours:
|
||||
return f"{hours} h {minutes:02d} min"
|
||||
return f"{minutes} min"
|
||||
|
||||
|
||||
def module_evolution(dct_progression):
|
||||
"""[(version, nb_modules, delta)] du plus ancien au plus récent.
|
||||
|
||||
Montre où les modules disparaissent : un saut qui en perd 15 d'un coup
|
||||
n'a pas le même sens qu'un saut qui n'en perd aucun.
|
||||
"""
|
||||
raw = dct_progression.get("dct_module_per_version") or {}
|
||||
rows = []
|
||||
for key, value in raw.items():
|
||||
version = _as_int(key)
|
||||
if version is not None and isinstance(value, list):
|
||||
rows.append((version, len(value)))
|
||||
rows.sort()
|
||||
out = []
|
||||
previous = None
|
||||
for version, count in rows:
|
||||
out.append(
|
||||
(version, count, None if previous is None else count - previous)
|
||||
)
|
||||
previous = count
|
||||
return out
|
||||
|
||||
|
||||
def cow_snapshots(private_dir, database_name):
|
||||
"""Instantanés de vues COW enregistrés, du plus ancien au plus récent."""
|
||||
directory = os.path.join(private_dir, database_name, "cow_snapshots")
|
||||
out = []
|
||||
for path in sorted(glob.glob(os.path.join(directory, "*.json"))):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"label": data.get("label") or os.path.basename(path)[:-5],
|
||||
"count": data.get("count"),
|
||||
"taken_at": data.get("taken_at") or "?",
|
||||
"path": path,
|
||||
}
|
||||
)
|
||||
out.sort(key=lambda item: item["taken_at"])
|
||||
return out
|
||||
|
||||
|
||||
def fix_hooks(ctx, global_dir):
|
||||
"""Correctifs de migration disponibles, et lesquels ont tourné."""
|
||||
applied = ctx.get("_fix_applied") or []
|
||||
out = []
|
||||
for index, item in enumerate(ctx.get("versions") or []):
|
||||
target = item["version"]
|
||||
stem = f"fix_migration_odoo{(target - 1) * 10}_to_odoo{target * 10}"
|
||||
found = [
|
||||
os.path.basename(p)
|
||||
for ext in (".sql", ".py")
|
||||
for p in [os.path.join(global_dir, stem + ext)]
|
||||
if os.path.exists(p)
|
||||
]
|
||||
if not found:
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"version": target,
|
||||
"file": found[0],
|
||||
"applied": bool(index < len(applied) and applied[index]),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def journal(dct_progression):
|
||||
"""Commandes exécutées et décisions annotées (les lignes « # »)."""
|
||||
lst = dct_progression.get("command_executed") or []
|
||||
comments = [
|
||||
c[2:].strip() for c in lst if isinstance(c, str) and c.startswith("# ")
|
||||
]
|
||||
commands = [
|
||||
c for c in lst if isinstance(c, str) and not c.startswith("# ")
|
||||
]
|
||||
return {"commands": commands, "comments": comments}
|
||||
|
||||
|
||||
def compute(
|
||||
dct_progression,
|
||||
ctx,
|
||||
database_name,
|
||||
read_uninstall,
|
||||
private_dir,
|
||||
global_dir,
|
||||
):
|
||||
"""Rassemble toutes les statistiques.
|
||||
|
||||
`read_uninstall(version, base)` est la lecture de liste de TodoUpgrade :
|
||||
elle résout ELLE-MÊME ses chemins privé puis global, ce qui garantit
|
||||
qu'on affiche exactement la liste qui serait appliquée. `private_dir` ne
|
||||
sert donc qu'aux instantanés COW, et `global_dir` qu'aux correctifs."""
|
||||
versions = [item["version"] for item in (ctx.get("versions") or [])]
|
||||
uninstall = {}
|
||||
for target in versions:
|
||||
try:
|
||||
_lst, detail = read_uninstall(target - 1, database_name)
|
||||
except Exception:
|
||||
detail = []
|
||||
if detail:
|
||||
uninstall[target] = detail
|
||||
|
||||
evolution = module_evolution(dct_progression)
|
||||
origin = dct_progression.get("lst_module_per_version_origin") or []
|
||||
return {
|
||||
"delay": fmt_delay(
|
||||
dct_progression.get("date_create"),
|
||||
dct_progression.get("date_update"),
|
||||
),
|
||||
"updated": dct_progression.get("date_update") or "?",
|
||||
"evolution": evolution,
|
||||
"origin_count": len(origin) if isinstance(origin, list) else 0,
|
||||
"missing": dct_progression.get("lst_module_missing") or [],
|
||||
"duplicate": dct_progression.get("lst_module_duplicate") or [],
|
||||
"uninstall": uninstall,
|
||||
"removed_total": sum(len(v) for v in uninstall.values()),
|
||||
"cow": cow_snapshots(private_dir, database_name),
|
||||
"fixes": fix_hooks(
|
||||
dict(
|
||||
ctx,
|
||||
_fix_applied=dct_progression.get(
|
||||
"state_4_fix_migration_odoo_lst"
|
||||
)
|
||||
or [],
|
||||
),
|
||||
global_dir,
|
||||
),
|
||||
"journal": journal(dct_progression),
|
||||
}
|
||||
|
||||
|
||||
def flat_module_list(uninstall):
|
||||
"""Tous les modules supprimés, dédupliqués, prêts à copier-coller."""
|
||||
seen = []
|
||||
for detail in uninstall.values():
|
||||
for item in detail:
|
||||
name = item[0] if isinstance(item, (list, tuple)) else item
|
||||
if name not in seen:
|
||||
seen.append(name)
|
||||
return seen
|
||||
|
|
@ -1561,6 +1561,110 @@ TRANSLATIONS = {
|
|||
"fr": "Rien de désinstallé.",
|
||||
"en": "Nothing uninstalled.",
|
||||
},
|
||||
"Migration statistics (read-only)": {
|
||||
"fr": "📊 Statistiques de la migration (lecture seule)",
|
||||
"en": "📊 Migration statistics (read-only)",
|
||||
},
|
||||
"Choice (0-3, default 1): ": {
|
||||
"fr": "Choix (0-3, défaut 1) : ",
|
||||
"en": "Choice (0-3, default 1): ",
|
||||
},
|
||||
"Migration statistics": {
|
||||
"fr": "Statistiques de la migration",
|
||||
"en": "Migration statistics",
|
||||
},
|
||||
"elapsed": {
|
||||
"fr": "écoulé",
|
||||
"en": "elapsed",
|
||||
},
|
||||
"Level reached": {
|
||||
"fr": "Niveau atteint",
|
||||
"en": "Level reached",
|
||||
},
|
||||
"Modules": {
|
||||
"fr": "Modules",
|
||||
"en": "Modules",
|
||||
},
|
||||
"modules": {
|
||||
"fr": "modules",
|
||||
"en": "modules",
|
||||
},
|
||||
"At the start": {
|
||||
"fr": "Au départ",
|
||||
"en": "At the start",
|
||||
},
|
||||
"Removed in total": {
|
||||
"fr": "Supprimés au total",
|
||||
"en": "Removed in total",
|
||||
},
|
||||
"Reported missing": {
|
||||
"fr": "Signalés manquants",
|
||||
"en": "Reported missing",
|
||||
},
|
||||
"Duplicated": {
|
||||
"fr": "En double",
|
||||
"en": "Duplicated",
|
||||
},
|
||||
"Migration fixes": {
|
||||
"fr": "Correctifs de migration",
|
||||
"en": "Migration fixes",
|
||||
},
|
||||
"COW views": {
|
||||
"fr": "Vues COW",
|
||||
"en": "COW views",
|
||||
},
|
||||
"views": {
|
||||
"fr": "vues",
|
||||
"en": "views",
|
||||
},
|
||||
"no snapshot": {
|
||||
"fr": "aucun instantané",
|
||||
"en": "no snapshot",
|
||||
},
|
||||
"Journal": {
|
||||
"fr": "Journal",
|
||||
"en": "Journal",
|
||||
},
|
||||
"commands": {
|
||||
"fr": "commandes",
|
||||
"en": "commands",
|
||||
},
|
||||
"recorded decisions": {
|
||||
"fr": "décisions annotées",
|
||||
"en": "recorded decisions",
|
||||
},
|
||||
"Removed modules, with their reason": {
|
||||
"fr": "Modules supprimés, avec leur justification",
|
||||
"en": "Removed modules, with their reason",
|
||||
},
|
||||
"Removed modules, comma-separated (copy)": {
|
||||
"fr": "Modules supprimés, séparés par des virgules (à copier)",
|
||||
"en": "Removed modules, comma-separated (copy)",
|
||||
},
|
||||
"COW views: snapshots and differences": {
|
||||
"fr": "Vues COW : instantanés et différences",
|
||||
"en": "COW views: snapshots and differences",
|
||||
},
|
||||
"Recorded decisions (journal)": {
|
||||
"fr": "Décisions annotées (journal)",
|
||||
"en": "Recorded decisions (journal)",
|
||||
},
|
||||
"Executed commands (last 30)": {
|
||||
"fr": "Commandes exécutées (30 dernières)",
|
||||
"en": "Executed commands (last 30)",
|
||||
},
|
||||
"nothing recorded": {
|
||||
"fr": "rien d'annoté",
|
||||
"en": "nothing recorded",
|
||||
},
|
||||
"Need two snapshots to diff.": {
|
||||
"fr": "Il faut deux instantanés pour comparer.",
|
||||
"en": "Need two snapshots to diff.",
|
||||
},
|
||||
"Diff which two? (e.g. 1,2 — blank to skip)": {
|
||||
"fr": "Comparer lesquels ? (ex. 1,2 — vide pour passer)",
|
||||
"en": "Diff which two? (e.g. 1,2 — blank to skip)",
|
||||
},
|
||||
"No migration in progress to resume.": {
|
||||
"fr": "Aucune migration en cours à reprendre.",
|
||||
"en": "No migration in progress to resume.",
|
||||
|
|
|
|||
|
|
@ -287,10 +287,13 @@ class TodoUpgrade:
|
|||
|
||||
@staticmethod
|
||||
def ask_ui():
|
||||
"""Interface of the migration: TUI or line-by-line prompts.
|
||||
"""Interface of the migration: TUI, line-by-line prompts, or the
|
||||
read-only statistics screen. Returns None to leave the tool.
|
||||
|
||||
The preference can settle it in advance (TODO > Configuration);
|
||||
« ask » asks. Same contract as the QEMU deployment.
|
||||
« ask » asks. Same contract as the QEMU deployment — except for
|
||||
« stats », which is never a stored default: it does nothing, so
|
||||
landing there every time would only be in the way.
|
||||
"""
|
||||
try:
|
||||
from script.todo import todo_prefs
|
||||
|
|
@ -303,9 +306,155 @@ class TodoUpgrade:
|
|||
print(f"\n{t('Interface:')}")
|
||||
print(f" [1] {t('TUI form')} *")
|
||||
print(f" [2] {t('Classic questions (line by line)')}")
|
||||
print(f" [3] {t('Migration statistics (read-only)')}")
|
||||
print(f" [0] {t('Cancel')}")
|
||||
print(f" {t('(change the default in TODO > Configuration)')}")
|
||||
answer = input(t("Choice (1-2, default 1): ")).strip()
|
||||
return "cli" if answer == "2" else "tui"
|
||||
answer = input(t("Choice (0-3, default 1): ")).strip()
|
||||
return {"0": None, "2": "cli", "3": "stats"}.get(answer, "tui")
|
||||
|
||||
def show_stats(self):
|
||||
"""Écran de statistiques, en lecture seule : rien n'est écrit, ni
|
||||
dans la base ni dans le journal de migration."""
|
||||
from script.todo import migration_stats as ms
|
||||
|
||||
if not os.path.exists(UPGRADE_DATABASE_CONFIG_LOG):
|
||||
print(f"\nℹ️ {t('No migration in progress to resume.')}")
|
||||
return
|
||||
dct = self.read_progression()
|
||||
if not dct:
|
||||
print(f"\nℹ️ {t('No migration in progress to resume.')}")
|
||||
return
|
||||
ctx = self.resume_context(dct)
|
||||
database_name = dct.get("config_database_name") or ""
|
||||
stats = ms.compute(
|
||||
dct,
|
||||
ctx,
|
||||
database_name,
|
||||
self.read_uninstall_module_list,
|
||||
PATH_MIGRATION_PRIVATE,
|
||||
PATH_MIGRATION_GLOBAL,
|
||||
)
|
||||
|
||||
while True:
|
||||
self.print_stats(ctx, stats)
|
||||
print(f"\n [1] {t('Removed modules, with their reason')}")
|
||||
print(f" [2] {t('Removed modules, comma-separated (copy)')}")
|
||||
print(f" [3] {t('COW views: snapshots and differences')}")
|
||||
print(f" [4] {t('Recorded decisions (journal)')}")
|
||||
print(f" [5] {t('Executed commands (last 30)')}")
|
||||
print(f" [0] {t('Back')}")
|
||||
answer = input(f"💬 {t('Your choice')} : ").strip()
|
||||
if answer in ("", "0"):
|
||||
return
|
||||
if answer == "1":
|
||||
for version, detail in sorted(stats["uninstall"].items()):
|
||||
print(f"\n── {version - 1}.0 → {version}.0 ──")
|
||||
self.print_uninstall_reason(detail)
|
||||
elif answer == "2":
|
||||
flat = ms.flat_module_list(stats["uninstall"])
|
||||
print(f"\n{len(flat)} {t('modules')} :\n")
|
||||
print(",".join(flat))
|
||||
elif answer == "3":
|
||||
self.stats_cow(stats, database_name)
|
||||
elif answer == "4":
|
||||
for line in stats["journal"]["comments"]:
|
||||
print(f" · {line}")
|
||||
if not stats["journal"]["comments"]:
|
||||
print(f" {t('nothing recorded')}")
|
||||
elif answer == "5":
|
||||
for line in stats["journal"]["commands"][-30:]:
|
||||
print(f" $ {line}")
|
||||
else:
|
||||
print(f"⚠️ {t('Unknown choice, continuing where it stopped')}.")
|
||||
|
||||
@staticmethod
|
||||
def print_stats(ctx, stats):
|
||||
"""Rend le tableau de bord de la migration."""
|
||||
print(f"\n📊 {t('Migration statistics')}")
|
||||
print(f" {t('File'):<11}: {ctx['file']}")
|
||||
print(
|
||||
f" {t('Database'):<11}: {ctx['database']}"
|
||||
f" · {t('Target')} : {ctx['target']}"
|
||||
)
|
||||
print(
|
||||
f" {t('Started'):<11}: {ctx['started']}"
|
||||
f" · {t('elapsed')} {stats['delay']}"
|
||||
)
|
||||
|
||||
print(f"\n── {t('Level reached')} ──")
|
||||
done = sum(1 for v in ctx["versions"] if v["done"])
|
||||
total = len(ctx["versions"]) or 1
|
||||
line = " "
|
||||
for item in ctx["versions"]:
|
||||
mark = "✅" if item["done"] else "⬜"
|
||||
line += f"{item['version'] - 1}.0→{item['version']}.0 {mark} "
|
||||
print(line)
|
||||
print(
|
||||
f" {done}/{len(ctx['versions'])} "
|
||||
f"{t('version bumps migrated')} ({done * 100 // total} %)"
|
||||
)
|
||||
print(
|
||||
" "
|
||||
+ " ".join(f"[{s['step']}]{s['icon']}" for s in ctx["steps"])
|
||||
)
|
||||
|
||||
print(f"\n── {t('Modules')} ──")
|
||||
if stats["origin_count"]:
|
||||
print(f" {t('At the start'):<24}: {stats['origin_count']}")
|
||||
for version, count, delta in stats["evolution"]:
|
||||
change = "" if delta is None else f" ({delta:+d})"
|
||||
print(f" {f'{version}.0':<24}: {count}{change}")
|
||||
print(f" {t('Removed in total'):<24}: {stats['removed_total']}")
|
||||
if stats["missing"]:
|
||||
print(f" {t('Reported missing'):<24}: {len(stats['missing'])}")
|
||||
if stats["duplicate"]:
|
||||
print(f" {t('Duplicated'):<24}: {len(stats['duplicate'])}")
|
||||
|
||||
if stats["fixes"]:
|
||||
print(f"\n── {t('Migration fixes')} ──")
|
||||
for fix in stats["fixes"]:
|
||||
mark = "✅" if fix["applied"] else "⬜"
|
||||
print(f" {mark} {fix['version']}.0 {fix['file']}")
|
||||
|
||||
print(f"\n── {t('COW views')} ──")
|
||||
if stats["cow"]:
|
||||
for snap in stats["cow"]:
|
||||
print(
|
||||
f" {snap['label']:<18} {str(snap['count']):>4} "
|
||||
f"{t('views')} {snap['taken_at']}"
|
||||
)
|
||||
else:
|
||||
print(f" {t('no snapshot')}")
|
||||
|
||||
print(f"\n── {t('Journal')} ──")
|
||||
print(
|
||||
f" {len(stats['journal']['commands'])} {t('commands')}, "
|
||||
f"{len(stats['journal']['comments'])} {t('recorded decisions')}"
|
||||
)
|
||||
|
||||
def stats_cow(self, stats, database_name):
|
||||
"""Instantanés COW, et différence entre deux d'entre eux."""
|
||||
snaps = stats["cow"]
|
||||
if len(snaps) < 2:
|
||||
print(f" {t('Need two snapshots to diff.')}")
|
||||
return
|
||||
for index, snap in enumerate(snaps, 1):
|
||||
print(
|
||||
f" [{index}] {snap['label']:<18} "
|
||||
f"{str(snap['count']):>4} {t('views')} {snap['taken_at']}"
|
||||
)
|
||||
raw = input(
|
||||
f"💬 {t('Diff which two? (e.g. 1,2 — blank to skip)')} : "
|
||||
).strip()
|
||||
parts = [p.strip() for p in raw.replace(",", " ").split()]
|
||||
if len(parts) != 2 or not all(p.isdigit() for p in parts):
|
||||
return
|
||||
first, second = (int(p) - 1 for p in parts)
|
||||
if not (0 <= first < len(snaps) and 0 <= second < len(snaps)):
|
||||
return
|
||||
self.diff_cow_views(
|
||||
database_name, snaps[first]["label"], snaps[second]["label"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def resume_tui(ctx):
|
||||
|
|
@ -722,7 +871,16 @@ class TodoUpgrade:
|
|||
self.dct_module_per_dct_version_path = {}
|
||||
default_database_name = "test"
|
||||
|
||||
use_tui = self.ask_ui() == "tui"
|
||||
# L'écran de statistiques ne fait rien : on y revient autant de fois
|
||||
# qu'on veut, et on repose ensuite le choix d'interface.
|
||||
while True:
|
||||
ui = self.ask_ui()
|
||||
if ui is None:
|
||||
return
|
||||
if ui != "stats":
|
||||
break
|
||||
self.show_stats()
|
||||
use_tui = ui == "tui"
|
||||
|
||||
if os.path.exists(UPGRADE_DATABASE_CONFIG_LOG):
|
||||
old_dct_progression = self.read_progression()
|
||||
|
|
|
|||
Loading…
Reference in a new issue