fix: afficher les noms lisibles des types de jours et des véhicules

- Filtre Jinja2 day_type_fr (WORK→Travail, TT→Télétravail, etc.)
- Appliqué dans entry_list.html et dashboard.html
- Passage de vehicles au template dashboard pour afficher vehicle.name
- Mise à jour du test test_entry_list en conséquence
- Ajout du plan docs/plans/2026-03-11-bareme-kilometrique.md
This commit is contained in:
2026-03-11 20:27:07 +01:00
parent 68ae347311
commit 859d94bfb1
7 changed files with 351 additions and 10 deletions

View File

@@ -36,6 +36,23 @@ _MOIS_FR = ["", "janvier", "février", "mars", "avril", "mai", "juin",
"juillet", "août", "septembre", "octobre", "novembre", "décembre"] "juillet", "août", "septembre", "octobre", "novembre", "décembre"]
_DAY_TYPE_LABELS = {
"WORK": "Travail",
"TT": "Télétravail",
"GARDE": "Garde",
"ASTREINTE": "Astreinte",
"FORMATION": "Formation",
"RTT": "RTT",
"CONGE": "Congé",
"MALADE": "Maladie",
"FERIE": "Férié",
}
def _day_type_fr(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'.""" """Formate une date en français : 'mercredi 11 mars 2026'."""
from datetime import date as date_type from datetime import date as date_type
@@ -64,6 +81,7 @@ def create_app(config_path=None):
db.init_app(app) db.init_app(app)
app.jinja_env.filters["date_fr"] = _date_fr app.jinja_env.filters["date_fr"] = _date_fr
app.jinja_env.filters["day_type_fr"] = _day_type_fr
from app.routes.dashboard import bp as dashboard_bp from app.routes.dashboard import bp as dashboard_bp
from app.routes.entries import bp as entries_bp from app.routes.entries import bp as entries_bp

View File

@@ -54,6 +54,7 @@ def index():
today=today, today=today,
today_entry=today_entry, today_entry=today_entry,
journeys=journeys, journeys=journeys,
vehicles=vehicles,
week_actual_str=minutes_to_str(week_actual), week_actual_str=minutes_to_str(week_actual),
week_balance=week_balance, week_balance=week_balance,
week_balance_str=minutes_to_str(abs(week_balance)), week_balance_str=minutes_to_str(abs(week_balance)),

View File

@@ -19,7 +19,7 @@
</div> </div>
<div class="flex items-center justify-between mt-3"> <div class="flex items-center justify-between mt-3">
<span class="text-xs font-semibold px-2 py-0.5 rounded" style="background:var(--parchment); color:#6A6258; letter-spacing:0.06em;"> <span class="text-xs font-semibold px-2 py-0.5 rounded" style="background:var(--parchment); color:#6A6258; letter-spacing:0.06em;">
{{ today_entry.day_type }}{% if today_entry.motor_vehicle_id %} · {{ today_entry.motor_vehicle_id }}{% endif %} {{ today_entry.day_type | day_type_fr }}{% if today_entry.motor_vehicle_id %} · {{ today_entry.motor_vehicle_id }}{% endif %}
</span> </span>
<a href="/entries/{{ today_entry.id }}/edit" <a href="/entries/{{ today_entry.id }}/edit"
class="text-xs font-semibold" style="color:var(--amber);">Modifier →</a> class="text-xs font-semibold" style="color:var(--amber);">Modifier →</a>
@@ -55,7 +55,7 @@
<div class="space-y-2 mt-1"> <div class="space-y-2 mt-1">
{% for vehicle_id, km in month_km.items() %} {% for vehicle_id, km in month_km.items() %}
<div class="flex items-baseline justify-between"> <div class="flex items-baseline justify-between">
<span class="text-xs" style="color:#8A8278;">{{ vehicle_id | capitalize }}</span> <span class="text-xs" style="color:#8A8278;">{{ vehicles[vehicle_id].name if vehicle_id in vehicles else vehicle_id | capitalize }}</span>
<span class="font-data font-semibold text-sm" style="color:var(--ink);">{{ km }} <span class="font-normal text-xs" style="color:#9A9288;">km</span></span> <span class="font-data font-semibold text-sm" style="color:var(--ink);">{{ km }} <span class="font-normal text-xs" style="color:#9A9288;">km</span></span>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -30,7 +30,7 @@
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="font-semibold text-sm" style="color:var(--ink);">{{ entry.date | date_fr }}</p> <p class="font-semibold text-sm" style="color:var(--ink);">{{ entry.date | date_fr }}</p>
<div class="flex items-center gap-2 mt-0.5"> <div class="flex items-center gap-2 mt-0.5">
<span class="text-xs font-semibold" style="color:{{ dot_color }}; letter-spacing:0.06em;">{{ entry.day_type }}</span> <span class="text-xs font-semibold" style="color:{{ dot_color }}; letter-spacing:0.06em;">{{ entry.day_type | day_type_fr }}</span>
{% if entry.time_slots %} {% if entry.time_slots %}
<span class="font-data text-xs" style="color:#8A8278;">{{ entry.total_hours_str() }}</span> <span class="font-data text-xs" style="color:#8A8278;">{{ entry.total_hours_str() }}</span>
{% endif %} {% endif %}

View File

@@ -1,21 +1,21 @@
[vehicles.citadine] [vehicles.citadine]
name = "Citadine électrique" name = "Twingo ZE"
fuel = "electric" fuel = "electric"
co2_per_km = 0 co2_per_km = 0
cv = 3 cv = 3
type = "moteur" type = "moteur"
[vehicles.familiale] [vehicles.familiale]
name = "Familiale thermique" name = "Duster"
fuel = "diesel" fuel = "diesel"
co2_per_km = 142 co2_per_km = 135
cv = 5 cv = 5
type = "moteur" type = "moteur"
[vehicles.moto] [vehicles.moto]
name = "Moto" name = "CE04"
fuel = "essence" fuel = "electric"
co2_per_km = 90 co2_per_km = 0
cv = 3 cv = 3
type = "moteur" type = "moteur"

View File

@@ -0,0 +1,322 @@
# Barème kilométrique — Refonte complète
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Corriger le barème kilométrique pour qu'il corresponde au barème fiscal officiel : tranches correctes (05 000 / 5 00120 000 / >20 000 km), CV individuels (3 CV, 4 CV, 5 CV, 6 CV, 7 CV+), et support de la majoration +20 % pour les véhicules électriques.
**Architecture:** Le barème est stocké dans `config.toml`, chargé via `get_bareme()` dans `config_loader.py`, et utilisé par `compute_frais_reels()` dans `travel_calc.py`. La majoration électrique sera un paramètre booléen de `compute_frais_reels()`, passé depuis `reports.py` en lisant `vehicle["fuel"] == "electric"`.
**Tech Stack:** Python 3, Flask, TOML, pytest
---
## Contexte
### Barème officiel 2025 (voitures)
| Puissance | ≤ 5 000 km | 5 00120 000 km | > 20 000 km |
|-----------|-------------------|--------------------------|---------------|
| 3 CV max | d × 0,529 | (d × 0,316) + 1 065 | d × 0,370 |
| 4 CV | d × 0,606 | (d × 0,340) + 1 330 | d × 0,407 |
| 5 CV | d × 0,636 | (d × 0,357) + 1 395 | d × 0,427 |
| 6 CV | d × 0,665 | (d × 0,374) + 1 457 | d × 0,447 |
| 7 CV+ | d × 0,697 | (d × 0,394) + 1 515 | d × 0,470 |
**Électrique :** majoration de +20 % sur le montant final calculé.
### Problèmes actuels dans config.toml
- Tranches fausses : 03 000 / 3 0016 000 / >6 000 (au lieu de 05 000 / 5 00120 000 / >20 000)
- Groupes CV incorrects : `cv_5` regroupe tout ≤ 5 CV
- Taux incorrects
- Électrique absent
---
## Tâche 1 : Mettre à jour les tests de `compute_frais_reels`
**Fichier :** `tests/test_travel_calc.py`
### Étape 1 : Remplacer `TRANCHES_CV5` par les tranches officielles et ajouter `TRANCHES_CV3`
Remplacer dans `tests/test_travel_calc.py` les constantes de test :
```python
TRANCHES_CV3 = [
{"km_max": 5000, "taux": 0.529, "forfait": 0},
{"km_max": 20000, "taux": 0.316, "forfait": 1065},
{"km_max": 0, "taux": 0.370, "forfait": 0},
]
TRANCHES_CV5 = [
{"km_max": 5000, "taux": 0.636, "forfait": 0},
{"km_max": 20000, "taux": 0.357, "forfait": 1395},
{"km_max": 0, "taux": 0.427, "forfait": 0},
]
```
### Étape 2 : Remplacer les assertions existantes et ajouter les nouveaux tests
```python
# Remplacer les tests frais_reels existants :
def test_frais_reels_tranche1():
result = compute_frais_reels(2000, TRANCHES_CV5)
assert abs(result - 1272.0) < 0.01 # 2000 × 0.636
def test_frais_reels_tranche2():
result = compute_frais_reels(10000, TRANCHES_CV5)
assert abs(result - 4965.0) < 0.01 # 10000 × 0.357 + 1395
def test_frais_reels_tranche3():
result = compute_frais_reels(25000, TRANCHES_CV5)
assert abs(result - 10675.0) < 0.01 # 25000 × 0.427
# Ajouter les tests électrique :
def test_frais_reels_electrique_majoration_20_pct():
result = compute_frais_reels(2000, TRANCHES_CV3, electric=True)
assert abs(result - 1269.6) < 0.01 # 2000 × 0.529 × 1.2
def test_frais_reels_non_electrique_sans_majoration():
result = compute_frais_reels(2000, TRANCHES_CV3, electric=False)
assert abs(result - 1058.0) < 0.01 # 2000 × 0.529
```
### Étape 3 : Vérifier que les tests échouent
```bash
.venv/bin/python -m pytest tests/test_travel_calc.py -v
```
Attendu : `FAILED` sur les tests `test_frais_reels_*` (mauvaises valeurs et paramètre `electric` inexistant).
---
## Tâche 2 : Modifier `compute_frais_reels` pour supporter la majoration électrique
**Fichier :** `app/business/travel_calc.py`
### Étape 1 : Ajouter le paramètre `electric`
Modifier la signature et la logique de `compute_frais_reels` :
```python
def compute_frais_reels(total_km_moteur: float, tranches: list[dict], electric: bool = False) -> float:
"""
Calcule les frais réels fiscaux selon le barème kilométrique.
km_max = 0 signifie "pas de limite" (dernière tranche).
electric=True applique la majoration de 20 % pour véhicules électriques.
"""
if not tranches or total_km_moteur <= 0:
return 0.0
for tranche in tranches:
km_max = tranche["km_max"]
if km_max == 0 or total_km_moteur <= km_max:
result = total_km_moteur * tranche["taux"] + tranche.get("forfait", 0)
return result * 1.2 if electric else result
last = tranches[-1]
result = total_km_moteur * last["taux"] + last.get("forfait", 0)
return result * 1.2 if electric else result
```
### Étape 2 : Vérifier que les tests passent
```bash
.venv/bin/python -m pytest tests/test_travel_calc.py -v
```
Attendu : tous les tests `PASSED`.
### Étape 3 : Commit
```bash
git -c commit.gpgsign=false add tests/test_travel_calc.py app/business/travel_calc.py
git -c commit.gpgsign=false commit -m "feat: compute_frais_reels supporte la majoration +20% électrique"
```
---
## Tâche 3 : Modifier `get_bareme()` pour les CV individuels
**Fichier :** `app/config_loader.py`
### Étape 1 : Remplacer la logique de groupement des CV
Remplacer la fonction `get_bareme` :
```python
def get_bareme(year: int, cv: int) -> list[dict]:
bareme = current_app.config.get("TOML", {}).get("bareme_kilometrique", {})
year_data = bareme.get(str(year), {})
if cv <= 3:
key = "cv_3"
elif cv == 4:
key = "cv_4"
elif cv == 5:
key = "cv_5"
elif cv == 6:
key = "cv_6"
else:
key = "cv_7plus"
return year_data.get(key, {}).get("tranches", [])
```
### Étape 2 : Vérifier que les tests existants passent toujours
```bash
.venv/bin/python -m pytest -v
```
Attendu : tous `PASSED` (les tests de routes utilisent la DB en mémoire et le TOML de test — pas le vrai barème).
---
## Tâche 4 : Mettre à jour `reports.py` pour passer le flag électrique
**Fichier :** `app/routes/reports.py`
### Étape 1 : Lire le champ `fuel` du véhicule et le passer à `compute_frais_reels`
Remplacer le bloc `frais_reels` :
```python
frais_reels = {}
for vehicle_id, km in total_km.items():
vehicle = vehicles.get(vehicle_id, {})
cv = vehicle.get("cv")
if cv:
tranches = get_bareme(year, cv)
electric = vehicle.get("fuel") == "electric"
frais_reels[vehicle_id] = round(compute_frais_reels(km, tranches, electric=electric), 2)
```
### Étape 2 : Vérifier les tests de routes
```bash
.venv/bin/python -m pytest tests/test_routes.py -v
```
Attendu : tous `PASSED`.
### Étape 3 : Commit
```bash
git -c commit.gpgsign=false add app/config_loader.py app/routes/reports.py
git -c commit.gpgsign=false commit -m "feat: get_bareme par CV individuel, majoration électrique dans reports"
```
---
## Tâche 5 : Corriger le barème dans `config.toml`
**Fichier :** `config.toml`
### Étape 1 : Remplacer entièrement la section `bareme_kilometrique`
Supprimer toutes les lignes `[[bareme_kilometrique.*]]` existantes et les remplacer par :
```toml
# --- Barème kilométrique voitures 2025 (revenus 2024) ---
# Source : https://www.service-public.gouv.fr/particuliers/actualites/A14686
# Majoration +20% pour véhicules électriques gérée dans travel_calc.py
[[bareme_kilometrique.2025.cv_3.tranches]]
km_max = 5000
taux = 0.529
forfait = 0
[[bareme_kilometrique.2025.cv_3.tranches]]
km_max = 20000
taux = 0.316
forfait = 1065
[[bareme_kilometrique.2025.cv_3.tranches]]
km_max = 0
taux = 0.370
forfait = 0
[[bareme_kilometrique.2025.cv_4.tranches]]
km_max = 5000
taux = 0.606
forfait = 0
[[bareme_kilometrique.2025.cv_4.tranches]]
km_max = 20000
taux = 0.340
forfait = 1330
[[bareme_kilometrique.2025.cv_4.tranches]]
km_max = 0
taux = 0.407
forfait = 0
[[bareme_kilometrique.2025.cv_5.tranches]]
km_max = 5000
taux = 0.636
forfait = 0
[[bareme_kilometrique.2025.cv_5.tranches]]
km_max = 20000
taux = 0.357
forfait = 1395
[[bareme_kilometrique.2025.cv_5.tranches]]
km_max = 0
taux = 0.427
forfait = 0
[[bareme_kilometrique.2025.cv_6.tranches]]
km_max = 5000
taux = 0.665
forfait = 0
[[bareme_kilometrique.2025.cv_6.tranches]]
km_max = 20000
taux = 0.374
forfait = 1457
[[bareme_kilometrique.2025.cv_6.tranches]]
km_max = 0
taux = 0.447
forfait = 0
[[bareme_kilometrique.2025.cv_7plus.tranches]]
km_max = 5000
taux = 0.697
forfait = 0
[[bareme_kilometrique.2025.cv_7plus.tranches]]
km_max = 20000
taux = 0.394
forfait = 1515
[[bareme_kilometrique.2025.cv_7plus.tranches]]
km_max = 0
taux = 0.470
forfait = 0
```
### Étape 2 : Lancer le serveur et vérifier visuellement la page `/reports/`
```bash
.venv/bin/python run.py
```
Naviguer sur `http://localhost:5000/reports/` et vérifier que les frais réels sont calculés (pas d'erreur 500, valeurs cohérentes).
### Étape 3 : Commit final
```bash
git -c commit.gpgsign=false add config.toml
git -c commit.gpgsign=false commit -m "fix: barème kilométrique 2025 — tranches et CV corrects, toutes puissances"
```
---
## Récapitulatif des fichiers modifiés
| Fichier | Nature |
|---|---|
| `tests/test_travel_calc.py` | Mise à jour des tranches de test et nouvelles assertions électrique |
| `app/business/travel_calc.py` | Paramètre `electric` dans `compute_frais_reels` |
| `app/config_loader.py` | `get_bareme()` avec CV individuels (cv_3 à cv_7plus) |
| `app/routes/reports.py` | Passage du flag `electric` à `compute_frais_reels` |
| `config.toml` | Barème complet et correct avec toutes les puissances 2025 |

View File

@@ -46,7 +46,7 @@ def test_entry_list(client, app):
response = client.get("/entries/") response = client.get("/entries/")
assert response.status_code == 200 assert response.status_code == 200
assert "TT" in response.text assert "Télétravail" in response.text
def test_reports_page(client): def test_reports_page(client):