doc: document configuration and application routes
Co-authored-by: OpenAI/GPT-5.6-Terra <vibecoder@antoineve.me> Co-authored-by: Google/Gemini-3.5-Flash <vibecoder@antoineve.me> Co-authored-by: DeepSeek/DeepSeek-v4-Flash <vibecoder@antoineve.me>
This commit is contained in:
@@ -1,3 +1,18 @@
|
||||
"""Package principal de l'application Flask.
|
||||
|
||||
Ce package initialise l'application Flask en utilisant le "Factory Pattern" (via la fonction `create_app`).
|
||||
Il configure également la base de données SQLite (via Flask-SQLAlchemy), charge la configuration TOML,
|
||||
enregistre les blueprints de routes, applique les migrations de schéma manuelles, et définit les filtres Jinja2 personnalisés.
|
||||
|
||||
Architecture et composants clés :
|
||||
1. Factory Pattern : `create_app` permet d'instancier l'application de manière isolée, facilitant les tests.
|
||||
2. Base de données : SQLite stockée dans `instance/worklog.db`.
|
||||
3. Migration de schéma : Gérée manuellement par `_migrate_db` sans utiliser Alembic.
|
||||
4. Filtres Jinja2 :
|
||||
- `date_fr` : Formate une date Python en chaîne lisible en français (ex: "mercredi 11 mars 2026").
|
||||
- `day_type_fr` : Traduit les codes internes des types de jours (ex: "WORK" -> "Travail").
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import sqlalchemy as sa
|
||||
@@ -9,7 +24,16 @@ db = SQLAlchemy()
|
||||
|
||||
|
||||
def _migrate_db(app):
|
||||
"""Applique les migrations de schéma manquantes (pas d'Alembic)."""
|
||||
"""Applique les migrations de schéma manquantes de manière incrémentale (sans Alembic).
|
||||
|
||||
Cette fonction vérifie l'état actuel de la base de données SQLite avant d'exécuter
|
||||
des instructions DDL (comme `ALTER TABLE`). Elle permet d'éviter les erreurs si la table
|
||||
`work_entries` n'existe pas encore (auquel cas `db.create_all()` s'en charge) ou si la
|
||||
colonne `motor_vehicle_id` a déjà été ajoutée lors d'un démarrage précédent.
|
||||
|
||||
Paramètres:
|
||||
app (Flask): L'instance de l'application Flask en cours d'initialisation.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
db_path = os.path.join(app.instance_path, "worklog.db")
|
||||
@@ -65,17 +89,57 @@ _DAY_TYPE_LABELS = {
|
||||
|
||||
|
||||
def _day_type_fr(code):
|
||||
"""Filtre Jinja2 pour traduire un code de type de jour en libellé français.
|
||||
|
||||
Exemple:
|
||||
`{{ 'WORK' | day_type_fr }}` -> "Travail"
|
||||
|
||||
Paramètres:
|
||||
code (str): Le code interne du type de jour (ex: "WORK", "TT", "GARDE").
|
||||
|
||||
Retourne:
|
||||
str: Le libellé en français correspondant, ou le code d'origine si aucune traduction n'est définie.
|
||||
"""
|
||||
return _DAY_TYPE_LABELS.get(code, code)
|
||||
|
||||
|
||||
def _date_fr(d):
|
||||
"""Formate une date en français : 'mercredi 11 mars 2026'."""
|
||||
"""Filtre Jinja2 pour formater une date en français lisible.
|
||||
|
||||
Exemple:
|
||||
`{{ entry.date | date_fr }}` -> "mercredi 11 mars 2026"
|
||||
|
||||
Paramètres:
|
||||
d (datetime.date): L'objet date à formater.
|
||||
|
||||
Retourne:
|
||||
str: La date formatée en français (jour de la semaine, jour du mois, mois en toutes lettres, année).
|
||||
"""
|
||||
jour = _JOURS_FR[d.weekday()]
|
||||
mois = _MOIS_FR[d.month]
|
||||
return f"{jour} {d.day} {mois} {d.year}"
|
||||
|
||||
|
||||
def create_app(config_path=None):
|
||||
"""Factory de création et de configuration de l'application Flask.
|
||||
|
||||
Cette fonction réalise les étapes suivantes :
|
||||
1. Instancie l'application Flask avec le support des configurations relatives à l'instance.
|
||||
2. Crée le dossier d'instance s'il n'existe pas.
|
||||
3. Configure l'URI de la base de données SQLite (`instance/worklog.db`).
|
||||
4. Charge la configuration TOML depuis `config.toml` (ou le chemin spécifié).
|
||||
5. Initialise l'extension Flask-SQLAlchemy (`db`).
|
||||
6. Enregistre les filtres Jinja2 personnalisés (`date_fr` et `day_type_fr`).
|
||||
7. Enregistre les blueprints de routes (`dashboard`, `entries`, `reports`).
|
||||
8. Exécute la migration de schéma manuelle (`_migrate_db`) puis crée les tables manquantes (`db.create_all()`).
|
||||
|
||||
Paramètres:
|
||||
config_path (str | None): Chemin optionnel vers le fichier de configuration TOML.
|
||||
Par défaut, cherche `config.toml` à la racine du projet.
|
||||
|
||||
Retourne:
|
||||
Flask: L'instance de l'application Flask configurée et prête à l'emploi.
|
||||
"""
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
os.makedirs(app.instance_path, exist_ok=True)
|
||||
|
||||
@@ -1,21 +1,76 @@
|
||||
"""Module de chargement et d'accès à la configuration TOML de l'application.
|
||||
|
||||
Ce module sert d'interface entre l'application Flask et le fichier `config.toml`.
|
||||
Toute la configuration des véhicules, des trajets et du barème kilométrique y est stockée
|
||||
et chargée au démarrage dans `app.config["TOML"]`.
|
||||
|
||||
Contrat TOML :
|
||||
1. Véhicules (`[vehicles]`) :
|
||||
- Chaque véhicule possède un identifiant unique (clé).
|
||||
- Attributs : `name` (nom d'affichage), `type` ("moteur" ou "velo"), `fuel` ("electric", "essence", etc.),
|
||||
et optionnellement `cv` (puissance fiscale pour le barème kilométrique).
|
||||
2. Trajets (`[journeys]`) :
|
||||
- Profils de trajets prédéfinis (ex: "domicile-travail").
|
||||
- Attributs : `name` (nom d'affichage), `distances` (dictionnaire associant un type de véhicule à une distance en km).
|
||||
3. Barème kilométrique (`[bareme_kilometrique.YYYY]`) :
|
||||
- Organisé par année (ex: "2026") puis par puissance fiscale (`cv_3`, `cv_4`, `cv_5`, `cv_6`, `cv_7plus`).
|
||||
- Chaque catégorie contient une liste de `tranches` définissant les formules de calcul des frais réels.
|
||||
- Une tranche possède : `km_max` (limite supérieure de la tranche, `km_max = 0` signifie "pas de limite supérieure"),
|
||||
`coeff` (coefficient multiplicateur par km) et `fixe` (montant forfaitaire à ajouter).
|
||||
4. Types de jours sans trajet :
|
||||
- Certains types de journées (Télétravail, Maladie, Congé, RTT, Férié) n'impliquent aucun déplacement physique.
|
||||
"""
|
||||
|
||||
from flask import current_app
|
||||
|
||||
|
||||
def get_vehicles():
|
||||
"""Récupère l'ensemble des véhicules configurés dans le fichier TOML.
|
||||
|
||||
Retourne:
|
||||
dict: Un dictionnaire des véhicules où la clé est l'identifiant du véhicule
|
||||
et la valeur est un dictionnaire contenant ses propriétés (name, type, fuel, cv).
|
||||
Retourne un dictionnaire vide si aucune configuration n'est chargée.
|
||||
"""
|
||||
return current_app.config.get("TOML", {}).get("vehicles", {})
|
||||
|
||||
|
||||
def get_motor_vehicles():
|
||||
"""Retourne uniquement les véhicules de type 'moteur'."""
|
||||
"""Filtre et retourne uniquement les véhicules de type 'moteur'.
|
||||
|
||||
Cette fonction exclut les véhicules alternatifs (comme les vélos) pour ne conserver
|
||||
que ceux qui possèdent une puissance fiscale (CV) et sont éligibles au barème kilométrique.
|
||||
|
||||
Retourne:
|
||||
dict: Un dictionnaire contenant uniquement les véhicules dont le type est 'moteur'.
|
||||
"""
|
||||
return {k: v for k, v in get_vehicles().items() if v.get("type") == "moteur"}
|
||||
|
||||
|
||||
def get_journeys():
|
||||
"""Récupère l'ensemble des profils de trajets configurés dans le fichier TOML.
|
||||
|
||||
Retourne:
|
||||
dict: Un dictionnaire des trajets où la clé est l'identifiant du trajet
|
||||
et la valeur est un dictionnaire contenant ses propriétés (name, distances).
|
||||
Retourne un dictionnaire vide si aucune configuration n'est chargée.
|
||||
"""
|
||||
return current_app.config.get("TOML", {}).get("journeys", {})
|
||||
|
||||
|
||||
def journey_has_motor(journey_profile_id: str | None) -> bool:
|
||||
"""Retourne True si le profil de trajet inclut un véhicule à moteur."""
|
||||
"""Vérifie si un profil de trajet donné inclut une distance pour véhicule à moteur.
|
||||
|
||||
Cette validation permet de déterminer si l'utilisateur doit sélectionner un véhicule
|
||||
à moteur lors de la saisie d'une journée de travail avec ce trajet.
|
||||
|
||||
Paramètres:
|
||||
journey_profile_id (str | None): L'identifiant du profil de trajet à vérifier.
|
||||
|
||||
Retourne:
|
||||
bool: True si le trajet existe et définit une distance pour la clé 'moteur',
|
||||
False sinon ou si l'identifiant est nul.
|
||||
"""
|
||||
if not journey_profile_id:
|
||||
return False
|
||||
journeys = get_journeys()
|
||||
@@ -24,6 +79,24 @@ def journey_has_motor(journey_profile_id: str | None) -> bool:
|
||||
|
||||
|
||||
def get_bareme(year: int, cv: int) -> list[dict]:
|
||||
"""Récupère les tranches du barème kilométrique pour une année et une puissance fiscale données.
|
||||
|
||||
Le barème kilométrique officiel est structuré en tranches de distances annuelles.
|
||||
Cette fonction sélectionne la bonne catégorie de puissance fiscale (CV) :
|
||||
- cv <= 3 -> 'cv_3'
|
||||
- cv == 4 -> 'cv_4'
|
||||
- cv == 5 -> 'cv_5'
|
||||
- cv == 6 -> 'cv_6'
|
||||
- cv >= 7 -> 'cv_7plus'
|
||||
|
||||
Paramètres:
|
||||
year (int): L'année civile concernée par le calcul.
|
||||
cv (int): La puissance fiscale du véhicule en chevaux fiscaux.
|
||||
|
||||
Retourne:
|
||||
list[dict]: Une liste de dictionnaires représentant les tranches applicables.
|
||||
Chaque tranche contient 'km_max' (0 si pas de limite), 'coeff' et 'fixe'.
|
||||
"""
|
||||
bareme = current_app.config.get("TOML", {}).get("bareme_kilometrique", {})
|
||||
year_data = bareme.get(str(year), {})
|
||||
if cv <= 3:
|
||||
@@ -40,4 +113,12 @@ def get_bareme(year: int, cv: int) -> list[dict]:
|
||||
|
||||
|
||||
def day_types_without_journey():
|
||||
"""Retourne l'ensemble des types de jours qui n'impliquent aucun trajet physique.
|
||||
|
||||
Ces types de jours (Télétravail, Maladie, Congé, RTT, Férié) sont exemptés de la saisie
|
||||
de trajets ou de véhicules, car le travail s'effectue à distance ou l'employé est absent.
|
||||
|
||||
Retourne:
|
||||
set[str]: Un ensemble de codes de types de jours (ex: {"TT", "MALADE", ...}).
|
||||
"""
|
||||
return {"TT", "MALADE", "CONGE", "RTT", "FERIE"}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Package contenant les blueprints de routes de l'application.
|
||||
|
||||
Ce package regroupe les différents modules de routes (blueprints) de l'application :
|
||||
- `dashboard` : Gère l'affichage du tableau de bord principal.
|
||||
- `entries` : Gère la saisie, la modification et la suppression des entrées de temps.
|
||||
- `reports` : Gère la génération des rapports d'activité annuels et mensuels.
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
"""Blueprint des routes du tableau de bord principal.
|
||||
|
||||
Ce module gère l'affichage de la page d'accueil (le tableau de bord). Il calcule et rassemble
|
||||
les statistiques clés de la semaine courante et du mois en cours, ainsi que le solde des congés
|
||||
et RTT pour l'année civile.
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
import sqlalchemy as sa
|
||||
@@ -15,6 +22,27 @@ bp = Blueprint("dashboard", __name__)
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
"""Affiche le tableau de bord principal de l'utilisateur.
|
||||
|
||||
Cette route effectue les opérations suivantes :
|
||||
1. Détermine la date du jour et calcule les limites de la semaine courante (du lundi au dimanche).
|
||||
2. Récupère toutes les entrées de temps (`WorkEntry`) de la semaine courante pour calculer :
|
||||
- Le temps de travail effectif cumulé (`week_actual`).
|
||||
- Le temps de travail de référence théorique (`week_ref`) selon le type de chaque journée.
|
||||
- Le solde d'heures de la semaine (`week_balance` = effectif - référence).
|
||||
3. Récupère toutes les entrées de temps du mois en cours (du 1er jour du mois jusqu'à aujourd'hui) pour calculer :
|
||||
- Les distances parcourues par véhicule (`month_km`) à partir des profils de trajets associés.
|
||||
- Les émissions de CO2 correspondantes (`month_co2`).
|
||||
4. Récupère ou initialise le solde annuel des congés et RTT (`balance`) et calcule les jours posés/utilisés (`used`).
|
||||
5. Vérifie s'il existe déjà une entrée de temps pour la journée d'aujourd'hui (`today_entry`).
|
||||
6. Rend le template `dashboard.html` avec l'ensemble de ces données de contexte.
|
||||
|
||||
Méthode HTTP :
|
||||
GET
|
||||
|
||||
Retourne:
|
||||
str: Le rendu HTML de la page du tableau de bord (`dashboard.html`).
|
||||
"""
|
||||
today = date.today()
|
||||
year = today.year
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
"""Blueprint des routes de gestion des entrées de temps (WorkEntry).
|
||||
|
||||
Ce module gère le cycle de vie complet des entrées journalières de travail :
|
||||
- Affichage de la liste historique des entrées.
|
||||
- Création d'une nouvelle entrée (formulaire et traitement POST).
|
||||
- Modification d'une entrée existante (formulaire pré-rempli et traitement POST).
|
||||
- Suppression d'une entrée.
|
||||
|
||||
Règles de validation et de cohérence des données :
|
||||
1. Unicité de la date : Une seule entrée (`WorkEntry`) est autorisée par jour.
|
||||
2. Types de jours sans trajet : Si le type de jour est dans `day_types_without_journey()` (TT, MALADE, CONGE, RTT, FERIE),
|
||||
le profil de trajet (`journey_profile_id`) est forcé à `None`.
|
||||
3. Véhicule à moteur : Si le profil de trajet sélectionné n'inclut pas de véhicule à moteur (vérifié via `journey_has_motor`),
|
||||
le véhicule à moteur (`motor_vehicle_id`) est forcé à `None`.
|
||||
4. Plages horaires : Les plages horaires (`TimeSlot`) existantes d'une entrée sont supprimées et recréées à chaque soumission
|
||||
pour simplifier la mise à jour des plages multiples.
|
||||
"""
|
||||
|
||||
from datetime import date, time
|
||||
|
||||
import sqlalchemy as sa
|
||||
@@ -29,6 +47,17 @@ DAY_TYPES = [
|
||||
|
||||
@bp.route("/")
|
||||
def list_entries():
|
||||
"""Affiche la liste historique de toutes les entrées de temps enregistrées.
|
||||
|
||||
Cette route récupère l'ensemble des entrées (`WorkEntry`) triées par date décroissante
|
||||
et les transmet au template pour affichage sous forme de tableau ou de liste.
|
||||
|
||||
Méthode HTTP :
|
||||
GET
|
||||
|
||||
Retourne:
|
||||
str: Le rendu HTML de la liste des entrées (`entry_list.html`).
|
||||
"""
|
||||
entries = db.session.scalars(sa.select(WorkEntry).order_by(WorkEntry.date.desc())).all()
|
||||
return render_template("entry_list.html", entries=entries)
|
||||
|
||||
@@ -36,6 +65,37 @@ def list_entries():
|
||||
@bp.route("/new", methods=["GET", "POST"])
|
||||
@bp.route("/<int:entry_id>/edit", methods=["GET", "POST"])
|
||||
def entry_form(entry_id=None):
|
||||
"""Gère l'affichage du formulaire et l'enregistrement (création ou modification) d'une entrée.
|
||||
|
||||
Cette route est doublement mappée pour la création (`/new`) et l'édition (`/<entry_id>/edit`).
|
||||
|
||||
Comportement en GET :
|
||||
- Si `entry_id` est fourni, récupère l'entrée correspondante en base de données. Si elle n'existe pas,
|
||||
affiche un message d'erreur et redirige vers la liste des entrées.
|
||||
- Prépare le contexte nécessaire au formulaire : liste des types de jours, profils de trajets,
|
||||
véhicules à moteur disponibles, types de jours sans trajet, et la date du jour par défaut.
|
||||
- Rend le template `entry_form.html`.
|
||||
|
||||
Comportement en POST :
|
||||
- Extrait et valide les données du formulaire : date, type de jour, trajet, véhicule à moteur, commentaire.
|
||||
- Applique les règles de cohérence (mise à `None` du trajet ou du véhicule si les conditions ne sont pas remplies).
|
||||
- En création : vérifie qu'aucune entrée n'existe déjà à cette date. Si c'est le cas, affiche une erreur.
|
||||
- Enregistre ou met à jour l'objet `WorkEntry` en base de données.
|
||||
- Supprime toutes les plages horaires (`TimeSlot`) existantes associées à cette entrée.
|
||||
- Parcourt les listes d'heures de début (`start_time`) et de fin (`end_time`) soumises, et recrée les objets
|
||||
`TimeSlot` valides associés à l'entrée.
|
||||
- Valide la transaction en base de données (`db.session.commit()`), affiche un message de succès
|
||||
et redirige vers le tableau de bord.
|
||||
|
||||
Paramètres:
|
||||
entry_id (int | None): L'identifiant de l'entrée à modifier, ou None pour une nouvelle entrée.
|
||||
|
||||
Méthodes HTTP :
|
||||
GET, POST
|
||||
|
||||
Retourne:
|
||||
str | Response: Le rendu HTML du formulaire (GET) ou une redirection HTTP (POST / erreur).
|
||||
"""
|
||||
entry = None
|
||||
if entry_id:
|
||||
entry = db.session.get(WorkEntry, entry_id)
|
||||
@@ -101,6 +161,21 @@ def entry_form(entry_id=None):
|
||||
|
||||
@bp.route("/<int:entry_id>/delete", methods=["POST"])
|
||||
def delete_entry(entry_id):
|
||||
"""Supprime une entrée de temps existante.
|
||||
|
||||
Cette route récupère l'entrée par son identifiant, la supprime de la base de données
|
||||
(les plages horaires associées sont également supprimées en cascade si configuré, ou gérées par SQLAlchemy),
|
||||
valide la transaction, affiche un message de succès et redirige vers la liste des entrées.
|
||||
|
||||
Paramètres:
|
||||
entry_id (int): L'identifiant de l'entrée à supprimer.
|
||||
|
||||
Méthode HTTP :
|
||||
POST (sécurisé contre les suppressions accidentelles via GET)
|
||||
|
||||
Retourne:
|
||||
Response: Une redirection HTTP vers la liste des entrées (`entries.list_entries`).
|
||||
"""
|
||||
entry = db.session.get(WorkEntry, entry_id)
|
||||
if entry:
|
||||
db.session.delete(entry)
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
"""Blueprint des routes de génération des rapports et statistiques.
|
||||
|
||||
Ce module gère l'affichage des rapports annuels et mensuels. Il calcule les distances cumulées,
|
||||
les émissions de CO2, les frais réels selon le barème kilométrique officiel, et compile les statistiques
|
||||
de temps de travail (médianes quotidiennes et hebdomadaires) pour chaque mois de l'année sélectionnée.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
|
||||
@@ -30,6 +37,35 @@ MONTHS_FR = {
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
"""Génère et affiche le rapport d'activité annuel et mensuel.
|
||||
|
||||
Cette route effectue les opérations suivantes :
|
||||
1. Récupère l'année cible depuis les paramètres de requête HTTP GET (`year`). Par défaut, utilise l'année en cours.
|
||||
2. Récupère toutes les entrées de temps (`WorkEntry`) de cette année civile.
|
||||
3. Récupère la configuration des véhicules et des trajets depuis le fichier TOML.
|
||||
4. Calcule les statistiques annuelles cumulées :
|
||||
- Distances parcourues par véhicule (`total_km`).
|
||||
- Émissions de CO2 totales en grammes (`total_co2`), converties ensuite en kilogrammes.
|
||||
- Frais réels remboursables par véhicule (`frais_reels`) en appliquant le barème kilométrique officiel
|
||||
de l'année correspondante (avec une majoration de +20% pour les véhicules électriques).
|
||||
- Nombre de jours par type de journée (`day_type_counts`).
|
||||
5. Regroupe les entrées par mois pour calculer les statistiques mensuelles :
|
||||
- Nom du mois en français.
|
||||
- Nombre de jours saisis.
|
||||
- Distances parcourues par véhicule et distance totale du mois.
|
||||
- Durée quotidienne médiane de travail et durée hebdomadaire médiane de travail (formatées en chaînes "HHhMM").
|
||||
6. Rend le template `reports.html` avec l'ensemble de ces données de contexte.
|
||||
|
||||
Méthode HTTP :
|
||||
GET
|
||||
|
||||
Paramètres de requête (Query Params) :
|
||||
year (int, optionnel) : L'année civile pour laquelle générer le rapport (ex: `?year=2026`).
|
||||
Par défaut, l'année courante.
|
||||
|
||||
Retourne:
|
||||
str: Le rendu HTML de la page des rapports (`reports.html`).
|
||||
"""
|
||||
year = request.args.get("year", date.today().year, type=int)
|
||||
start = date(year, 1, 1)
|
||||
end = date(year, 12, 31)
|
||||
|
||||
Reference in New Issue
Block a user