diff --git a/docs/specs.md b/docs/specs.md new file mode 100644 index 0000000..9897e77 --- /dev/null +++ b/docs/specs.md @@ -0,0 +1,135 @@ +csv-to-timemanager.py — Spécifications d'importation +==================================================== + +Objectif +-------- + +Le script `csv-to-timemanager.py` lit des fichiers CSV configurés dans `config.json` et envoie des requêtes vers l'API Nextcloud TimeManager pour créer ou mettre à jour : + +- clients, +- projets, +- tâches, +- entrées de temps. + +Il utilise aussi une base MySQL locale pour vérifier l'existence des objets et éviter les doublons. + +Sources des données +------------------- + +Le script traite les entrées de chaque CSV listé sous `config['calendars']`. +Chaque entrée doit contenir au minimum : + +- `csv_name` : chemin du fichier CSV à importer, +- `user_name` : login Nextcloud utilisé pour authentifier les requêtes, +- `api_password` : mot de passe ou mot de passe d'application Nextcloud. + +Colonnes CSV utilisées +---------------------- + +Pour chaque ligne du CSV, le script utilise précisément ces colonnes : + +- `Description` → nom du client TimeManager +- `Categories` → nom du projet TimeManager +- `Titre` → nom de la tâche TimeManager +- `Date debut` → date de début de l'entrée de temps +- `Heure debut` → heure de début de l'entrée de temps +- `Date fin` → date de fin de l'entrée de temps +- `Heure fin` → heure de fin de l'entrée de temps + +Correspondance vers TimeManager +-------------------------------- + +| Colonne CSV | Entité TimeManager | Champ TimeManager / usage | +|------------------|--------------------|---------------------------| +| `Description` | client | `name` du client | +| `Categories` | project | `name` du projet | +| `Titre` | task | `name` de la tâche | +| `Date debut` | time entry | `start` (date + heure) | +| `Heure debut` | time entry | `start` (date + heure) | +| `Date fin` | time entry | `end` (date + heure) | +| `Heure fin` | time entry | `end` (date + heure) | + +Traitements appliqués +---------------------- + +1. Lecture du CSV + - Le fichier est lu avec `csv.reader(..., delimiter=';')`. + - La première ligne est traitée comme en-tête et sert à associer chaque colonne à une clé de dictionnaire. + +2. Nettoyage des chaînes de texte + - `sanitize(chain)` : + - retire tous les caractères sauf lettres, chiffres et espaces, + - applique `unidecode` pour normaliser les accents, + - `strip()` pour enlever les espaces en début/fin, + - `capitalize()` pour mettre la première lettre en majuscule. + - `sanitize_date(chain)` : + - conserve aussi les caractères `-` et `:` supplémentaires (utile pour les dates/heures), + - applique les mêmes normalisations que `sanitize`. + +3. Création / existence d'objets + - **Chaque entité est créée indépendamment** si elle n'existe pas : + - Si le client `Description` n'existe pas, il est créé via `create_client()`. + - Si le projet `Categories` n'existe pas (pour ce client), il est créé via `create_project()`. + - Si la tâche `Titre` n'existe pas (pour ce projet), elle est créée via `create_task()`. + - Les actions de création appellent l'API `updateObjects` avec un payload minimal contenant l'entité concernée. + +4. Insertion d'une entrée de temps + - Le script construit `start` et `end` en concaténant `Date debut + Heure debut` et `Date fin + Heure fin`. + - Il envoie ensuite un objet `times.created` à l'API `updateObjects`. + +Validation et gestion des doublons +--------------------------------- + +- Avant d'ajouter une entrée de temps, le script vérifie si une entrée identique existe déjà dans la base : + - il cherche d'abord une entrée avec les mêmes `user_id`, `start` (date+heure début) et `end` (date+heure fin). + - si une telle entrée existe, il valide que le `task`, le `project` et le `client` correspondent. + - si tous les critères correspondent, l'import est ignoré pour éviter un doublon. +- **Important** : deux entrées avec les mêmes client/projet/tâche mais à des **heures différentes** créent 2 entrées TimeManager distinctes (elles ne sont pas considérées comme des doublons). + +Gestion des données manquantes +----------------------------- + +- Si `user_name` ou `api_password` manque dans la configuration pour un calendrier, ce calendrier est ignoré. +- Dans le CSV, si `client_name` (`Description`) et `project_name` (`Categories`) sont tous deux vides, la ligne est ignorée. +- Si le client existe mais que son UUID ne peut pas être récupéré après création, le traitement de la ligne est abandonné avec un message d'erreur. +- **Limitations actuelles (risque)** : Le script ne valide pas que `Titre`, `Date debut`, `Heure debut`, `Date fin` ou `Heure fin` sont présents : + - Si `Titre` est vide, une tâche avec un nom vide peut être créée ou recherchée. + - Si `Date debut`, `Heure debut`, `Date fin` ou `Heure fin` sont vides, une entrée de temps avec `start` ou `end` incomplet sera créée. + - Ce comportement est **indésirable** et devrait être amélioré (voir `docs/todo.md`). + +Cas particuliers +---------------- + +- Pour l'existence d'un client/projet/tâche, la comparaison se fait avec les noms nettoyés (`sanitize`) et uniquement les objets dont `status != "deleted"`. +- `create_client()` retourne maintenant l'UUID extrait de la réponse API si disponible. Si la réponse ne contient pas d'UUID, le script effectue une seconde récupération via la base de données. +- `create_project()` et `create_task()` ne retournent pas d'UUID : ils comptent sur les recherches suivantes (`get_project_uuid_by_name`, `get_task_uuid_by_name`) pour retrouver l'objet créé. + +Notes d'implémentation +---------------------- + +- Les requêtes vers TimeManager utilisent toujours `POST /api/updateObjects`. +- Les objets envoyés contiennent des champs vides (`uuid`, `commit`, `created`, `changed`) pour laisser le serveur les remplir si nécessaire. +- Le script n'envoie pas de données de `deleted` ou `updated` : il se contente de créer des objets dans `created`. + +Résumé rapide +------------- + +Le script transforme chaque ligne CSV en un client/projet/tâche puis une entrée de temps, en appliquant un nettoyage de texte simple et en évitant le double-import des mêmes périodes. + +Champs obligatoires pour créer une entrée de temps +------------------------------------------------- + +Pour qu'une ligne CSV soit importée avec succès en tant qu'entrée de temps TimeManager, les champs suivants **doivent** être renseignés : + +- `Description` : non vide (nom du client) OU `Categories` : non vide (nom du projet) — au moins un des deux. +- `Categories` : non vide (nom du projet). +- `Titre` : non vide (nom de la tâche). +- `Date debut` : non vide et valide. +- `Heure debut` : non vide et valide. +- `Date fin` : non vide et valide. +- `Heure fin` : non vide et valide. + +**Note actuellement** : le script ne valide pas strictement ces conditions. Il est recommandé d'améliorer ce point (voir `docs/todo.md`). + +Le point clef est que seules les colonnes suivantes sont exploitées : +`Description`, `Categories`, `Titre`, `Date debut`, `Heure debut`, `Date fin`, `Heure fin`. diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..298b1e4 --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,78 @@ +Tâches d'amélioration — csv-to-timemanager.py +============================================== + +Validation des champs obligatoires +----------------------------------- + +### TODO : Améliorer la validation des champs CSV + +**Actuellement** : le script n'effectue pas de validation stricte sur les champs obligatoires. Cela peut conduire à : +- Créer des tâches avec un nom vide (si `Titre` manque). +- Créer des entrées de temps avec `start` ou `end` incomplets (si dates/heures manquent). + +**À faire** : +1. Ajouter une validation au début de la boucle de traitement des lignes CSV pour vérifier que les champs suivants sont non-vides **et valides** : + - `Titre` : non vide après nettoyage. + - `Date debut` : non vide et correspond à un format date valide. + - `Heure debut` : non vide et correspond à un format heure valide (ex: `HH:MM` ou `HH:MM:SS`). + - `Date fin` : non vide et correspond à un format date valide. + - `Heure fin` : non vide et correspond à un format heure valide. + +2. Condition d'import : une ligne est importée **seulement si** : + - `Description` OU `Categories` sont renseignés (au moins un), + - ET tous les champs listés ci-dessus sont valides. + +3. Pour les lignes invalides : enregistrer une ligne de log / warning avec la raison (ex: "Titre manquant", "Date debut invalide"), pour faciliter le débogage. + +### TEST : Vérifier qu'aucune tâche n'est créée avec champs manquants + +**Objectif** : S'assurer que le code corrigé ne crée pas de tâches/entrées de temps incomplètes. + +**À faire** : +1. Créer un petit fichier CSV de test contenant des lignes avec champs manquants (ex: `Titre` vide, `Date debut` manquant, etc.). +2. Exécuter le script avec ce fichier de test. +3. Vérifier dans la base de données ou via l'interface TimeManager que **aucune** tâche ou entrée de temps incomplète n'a été créée. +4. Documenter les résultats du test dans un fichier `tests/csv_validation_test.md`. + +### Alternative : Utiliser la validation existante dans TimeManager + +Si l'API TimeManager rejette les entrées incomplètes, le script bénéficiera indirectement de cette validation. +À vérifier auprès du serveur TimeManager (réponse HTTP 406 / erreur de validation de la requête). + +Gestion des modifications utilisateur et réimport +-------------------------------------------------- + +### TODO : Analyser le comportement en cas de modification manuelle + réimport + +**Problème** : Que se passe-t-il si un utilisateur modifie une tâche ou une entrée de temps directement dans TimeManager, puis que le script réimporte le même CSV ? + +**Cas d'usage** : +1. Un CSV est importé, créant une tâche `Nom original` avec une entrée de temps. +2. L'utilisateur renomme la tâche à `Nom modifié` dans TimeManager. +3. Le CSV est réimporté avec la même ligne. + +**Comportement actuel** : +- Le script utilise la colonne `Titre` du CSV (ex: `Nom original`) pour chercher la tâche existante. +- Si la tâche a été renommée (ex: `Nom modifié`), le script ne la trouvera **pas** (car `sanitize("Nom original") != sanitize("Nom modifié")`). +- Le script créera une **nouvelle tâche** `Nom original`, créant ainsi un doublon. +- L'ancienne tâche (`Nom modifié`) restera en place dans TimeManager. + +**Risques** : +- Création involontaire de doublons. +- Incohérence entre le CSV et TimeManager. +- Perte de données si les modifications de l'utilisateur n'ont pas été sauvegardées ailleurs. + +**À considérer** : +1. Faut-il implémenter une stratégie de fusionnement / synchronisation bidirectionnelle (ex: tracking des UUIDs) ? +2. Faut-il documenter que le script n'est pas conçu pour gérer les modifications manuelles post-import ? +3. Faut-il ajouter une option `--allow-duplicates` ou `--force-sync` au script pour expliciter ce comportement ? +4. Analyser comment l'app TimeManager gère les commits et les conflits (voir `lib/Db/StorageHelper.php`). + +**Priorité** : À discuter avec l'équipe pour clarifier le flux de travail prévu. + +Références +---------- +- Détail de la validation : voir `docs/specs.md` section "Gestion des données manquantes". +- Code de validation des lignes : voir `csv-to-timemanager.py` ligne ~437. +- Détection d'existence de tâches : voir `csv-to-timemanager.py` fonctions `task_exist_by_name`, `project_exist_by_name`, `client_exist_by_name`. +- Stratégie de sync TimeManager : voir `lib/Db/StorageHelper.php` (analyse du système de commits et résolution de conflits).