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 os
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@@ -9,7 +24,16 @@ db = SQLAlchemy()
|
|||||||
|
|
||||||
|
|
||||||
def _migrate_db(app):
|
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
|
import sqlite3
|
||||||
|
|
||||||
db_path = os.path.join(app.instance_path, "worklog.db")
|
db_path = os.path.join(app.instance_path, "worklog.db")
|
||||||
@@ -65,17 +89,57 @@ _DAY_TYPE_LABELS = {
|
|||||||
|
|
||||||
|
|
||||||
def _day_type_fr(code):
|
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)
|
return _DAY_TYPE_LABELS.get(code, code)
|
||||||
|
|
||||||
|
|
||||||
def _date_fr(d):
|
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()]
|
jour = _JOURS_FR[d.weekday()]
|
||||||
mois = _MOIS_FR[d.month]
|
mois = _MOIS_FR[d.month]
|
||||||
return f"{jour} {d.day} {mois} {d.year}"
|
return f"{jour} {d.day} {mois} {d.year}"
|
||||||
|
|
||||||
|
|
||||||
def create_app(config_path=None):
|
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)
|
app = Flask(__name__, instance_relative_config=True)
|
||||||
|
|
||||||
os.makedirs(app.instance_path, exist_ok=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
|
from flask import current_app
|
||||||
|
|
||||||
|
|
||||||
def get_vehicles():
|
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", {})
|
return current_app.config.get("TOML", {}).get("vehicles", {})
|
||||||
|
|
||||||
|
|
||||||
def get_motor_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"}
|
return {k: v for k, v in get_vehicles().items() if v.get("type") == "moteur"}
|
||||||
|
|
||||||
|
|
||||||
def get_journeys():
|
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", {})
|
return current_app.config.get("TOML", {}).get("journeys", {})
|
||||||
|
|
||||||
|
|
||||||
def journey_has_motor(journey_profile_id: str | None) -> bool:
|
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:
|
if not journey_profile_id:
|
||||||
return False
|
return False
|
||||||
journeys = get_journeys()
|
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]:
|
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", {})
|
bareme = current_app.config.get("TOML", {}).get("bareme_kilometrique", {})
|
||||||
year_data = bareme.get(str(year), {})
|
year_data = bareme.get(str(year), {})
|
||||||
if cv <= 3:
|
if cv <= 3:
|
||||||
@@ -40,4 +113,12 @@ def get_bareme(year: int, cv: int) -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def day_types_without_journey():
|
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"}
|
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
|
from datetime import date, timedelta
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@@ -15,6 +22,27 @@ bp = Blueprint("dashboard", __name__)
|
|||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
def index():
|
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()
|
today = date.today()
|
||||||
year = today.year
|
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
|
from datetime import date, time
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@@ -29,6 +47,17 @@ DAY_TYPES = [
|
|||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
def list_entries():
|
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()
|
entries = db.session.scalars(sa.select(WorkEntry).order_by(WorkEntry.date.desc())).all()
|
||||||
return render_template("entry_list.html", entries=entries)
|
return render_template("entry_list.html", entries=entries)
|
||||||
|
|
||||||
@@ -36,6 +65,37 @@ def list_entries():
|
|||||||
@bp.route("/new", methods=["GET", "POST"])
|
@bp.route("/new", methods=["GET", "POST"])
|
||||||
@bp.route("/<int:entry_id>/edit", methods=["GET", "POST"])
|
@bp.route("/<int:entry_id>/edit", methods=["GET", "POST"])
|
||||||
def entry_form(entry_id=None):
|
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
|
entry = None
|
||||||
if entry_id:
|
if entry_id:
|
||||||
entry = db.session.get(WorkEntry, 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"])
|
@bp.route("/<int:entry_id>/delete", methods=["POST"])
|
||||||
def delete_entry(entry_id):
|
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)
|
entry = db.session.get(WorkEntry, entry_id)
|
||||||
if entry:
|
if entry:
|
||||||
db.session.delete(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 collections import defaultdict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
@@ -30,6 +37,35 @@ MONTHS_FR = {
|
|||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
def index():
|
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)
|
year = request.args.get("year", date.today().year, type=int)
|
||||||
start = date(year, 1, 1)
|
start = date(year, 1, 1)
|
||||||
end = date(year, 12, 31)
|
end = date(year, 12, 31)
|
||||||
|
|||||||
Reference in New Issue
Block a user