chore: fix python style violations

Co-authored-by: OpenAI/GPT-5.6-Terra <vibecoder@antoineve.me>
Co-authored-by: OpenAI/GPT-5.6-Luna <vibecoder@antoineve.me>
Co-authored-by: MiniMax/MiniMax-M3 <vibecoder@antoineve.me>
Co-authored-by: DeepSeek/DeepSeek-v4-Flash <vibecoder@antoineve.me>
This commit is contained in:
2026-08-13 10:14:21 +02:00
parent 82beb6241f
commit c7a0a77d1f
14 changed files with 227 additions and 160 deletions

View File

@@ -1,8 +1,9 @@
import os
import sqlalchemy as sa
import tomllib
from flask import Flask from flask import Flask
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
import tomllib
import os
import sqlalchemy as sa
db = SQLAlchemy() db = SQLAlchemy()
@@ -10,6 +11,7 @@ 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 (pas d'Alembic)."""
import sqlite3 import sqlite3
db_path = os.path.join(app.instance_path, "worklog.db") db_path = os.path.join(app.instance_path, "worklog.db")
if not os.path.exists(db_path): if not os.path.exists(db_path):
return # Nouvelle DB, create_all() s'en charge return # Nouvelle DB, create_all() s'en charge
@@ -25,27 +27,40 @@ def _migrate_db(app):
engine = db.engine engine = db.engine
if "motor_vehicle_id" not in columns: if "motor_vehicle_id" not in columns:
with engine.connect() as conn: with engine.connect() as conn:
conn.execute(sa.text( conn.execute(
"ALTER TABLE work_entries ADD COLUMN motor_vehicle_id VARCHAR(64)" sa.text("ALTER TABLE work_entries ADD COLUMN motor_vehicle_id VARCHAR(64)")
)) )
conn.commit() conn.commit()
_JOURS_FR = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"] _JOURS_FR = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
_MOIS_FR = ["", "janvier", "février", "mars", "avril", "mai", "juin", _MOIS_FR = [
"juillet", "août", "septembre", "octobre", "novembre", "décembre"] "",
"janvier",
"février",
"mars",
"avril",
"mai",
"juin",
"juillet",
"août",
"septembre",
"octobre",
"novembre",
"décembre",
]
_DAY_TYPE_LABELS = { _DAY_TYPE_LABELS = {
"WORK": "Travail", "WORK": "Travail",
"TT": "Télétravail", "TT": "Télétravail",
"GARDE": "Garde", "GARDE": "Garde",
"ASTREINTE": "Astreinte", "ASTREINTE": "Astreinte",
"FORMATION": "Formation", "FORMATION": "Formation",
"RTT": "RTT", "RTT": "RTT",
"CONGE": "Congé", "CONGE": "Congé",
"MALADE": "Maladie", "MALADE": "Maladie",
"FERIE": "Férié", "FERIE": "Férié",
} }
@@ -55,7 +70,6 @@ def _day_type_fr(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
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}"
@@ -66,7 +80,9 @@ def create_app(config_path=None):
os.makedirs(app.instance_path, exist_ok=True) os.makedirs(app.instance_path, exist_ok=True)
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}" app.config["SQLALCHEMY_DATABASE_URI"] = (
f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}"
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-change-in-prod") app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-change-in-prod")
@@ -86,6 +102,7 @@ def create_app(config_path=None):
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
from app.routes.reports import bp as reports_bp from app.routes.reports import bp as reports_bp
app.register_blueprint(dashboard_bp) app.register_blueprint(dashboard_bp)
app.register_blueprint(entries_bp) app.register_blueprint(entries_bp)
app.register_blueprint(reports_bp) app.register_blueprint(reports_bp)

View File

@@ -1,34 +1,40 @@
from app import db
from app.models import WorkEntry, LeaveBalance
import sqlalchemy as sa
from datetime import date from datetime import date
import sqlalchemy as sa
from app import db
from app.models import LeaveBalance, WorkEntry
def compute_leave_used(year: int) -> dict[str, int]: def compute_leave_used(year: int) -> dict[str, int]:
start = date(year, 1, 1) start = date(year, 1, 1)
end = date(year, 12, 31) end = date(year, 12, 31)
conges = db.session.scalar( conges = (
sa.select(sa.func.count()).where( db.session.scalar(
WorkEntry.date.between(start, end), sa.select(sa.func.count()).where(
WorkEntry.day_type == "CONGE", WorkEntry.date.between(start, end),
WorkEntry.day_type == "CONGE",
)
) )
) or 0 or 0
)
rtt = db.session.scalar( rtt = (
sa.select(sa.func.count()).where( db.session.scalar(
WorkEntry.date.between(start, end), sa.select(sa.func.count()).where(
WorkEntry.day_type == "RTT", WorkEntry.date.between(start, end),
WorkEntry.day_type == "RTT",
)
) )
) or 0 or 0
)
return {"conges": conges, "rtt": rtt} return {"conges": conges, "rtt": rtt}
def get_or_create_balance(year: int) -> LeaveBalance: def get_or_create_balance(year: int) -> LeaveBalance:
balance = db.session.scalar( balance = db.session.scalar(sa.select(LeaveBalance).where(LeaveBalance.year == year))
sa.select(LeaveBalance).where(LeaveBalance.year == year)
)
if balance is None: if balance is None:
balance = LeaveBalance(year=year) balance = LeaveBalance(year=year)
db.session.add(balance) db.session.add(balance)

View File

@@ -32,7 +32,9 @@ def compute_co2_grams(km_by_vehicle: dict[str, int], vehicles: dict) -> float:
return total return total
def compute_frais_reels(total_km_moteur: float, tranches: list[dict], electric: bool = False) -> float: 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. Calcule les frais réels fiscaux selon le barème kilométrique.
km_max = 0 signifie "pas de limite" (dernière tranche). km_max = 0 signifie "pas de limite" (dernière tranche).

View File

@@ -1,8 +1,10 @@
from app import db from datetime import date, datetime, time
from datetime import date, time, datetime
import sqlalchemy as sa import sqlalchemy as sa
import sqlalchemy.orm as so import sqlalchemy.orm as so
from app import db
class WorkEntry(db.Model): class WorkEntry(db.Model):
__tablename__ = "work_entries" __tablename__ = "work_entries"

View File

@@ -1,12 +1,14 @@
from flask import Blueprint, render_template
from datetime import date, timedelta from datetime import date, timedelta
import sqlalchemy as sa import sqlalchemy as sa
from flask import Blueprint, render_template
from app import db from app import db
from app.models import WorkEntry
from app.business.time_calc import minutes_to_str, work_minutes_reference
from app.business.travel_calc import compute_km_for_entry, compute_co2_grams
from app.business.leave_calc import compute_leave_used, get_or_create_balance from app.business.leave_calc import compute_leave_used, get_or_create_balance
from app.config_loader import get_vehicles, get_journeys from app.business.time_calc import minutes_to_str, work_minutes_reference
from app.business.travel_calc import compute_co2_grams, compute_km_for_entry
from app.config_loader import get_journeys, get_vehicles
from app.models import WorkEntry
bp = Blueprint("dashboard", __name__) bp = Blueprint("dashboard", __name__)
@@ -45,9 +47,7 @@ def index():
balance = get_or_create_balance(year) balance = get_or_create_balance(year)
used = compute_leave_used(year) used = compute_leave_used(year)
today_entry = db.session.scalar( today_entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == today))
sa.select(WorkEntry).where(WorkEntry.date == today)
)
return render_template( return render_template(
"dashboard.html", "dashboard.html",

View File

@@ -1,9 +1,16 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from datetime import date, time from datetime import date, time
import sqlalchemy as sa import sqlalchemy as sa
from flask import Blueprint, flash, redirect, render_template, request, url_for
from app import db from app import db
from app.models import WorkEntry, TimeSlot from app.config_loader import (
from app.config_loader import get_journeys, get_motor_vehicles, day_types_without_journey, journey_has_motor day_types_without_journey,
get_journeys,
get_motor_vehicles,
journey_has_motor,
)
from app.models import TimeSlot, WorkEntry
bp = Blueprint("entries", __name__, url_prefix="/entries") bp = Blueprint("entries", __name__, url_prefix="/entries")
@@ -22,9 +29,7 @@ DAY_TYPES = [
@bp.route("/") @bp.route("/")
def list_entries(): def list_entries():
entries = db.session.scalars( entries = db.session.scalars(sa.select(WorkEntry).order_by(WorkEntry.date.desc())).all()
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)
@@ -51,9 +56,7 @@ def entry_form(entry_id=None):
journey_profile_id = None journey_profile_id = None
if entry is None: if entry is None:
existing = db.session.scalar( existing = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == entry_date))
sa.select(WorkEntry).where(WorkEntry.date == entry_date)
)
if existing: if existing:
flash(f"Une entrée existe déjà pour le {entry_date}.", "error") flash(f"Une entrée existe déjà pour le {entry_date}.", "error")
return redirect(url_for("entries.entry_form")) return redirect(url_for("entries.entry_form"))
@@ -72,11 +75,13 @@ def entry_form(entry_id=None):
ends = request.form.getlist("end_time") ends = request.form.getlist("end_time")
for s, e in zip(starts, ends): for s, e in zip(starts, ends):
if s and e: if s and e:
db.session.add(TimeSlot( db.session.add(
entry=entry, TimeSlot(
start_time=time.fromisoformat(s), entry=entry,
end_time=time.fromisoformat(e), start_time=time.fromisoformat(s),
)) end_time=time.fromisoformat(e),
)
)
db.session.commit() db.session.commit()
flash("Entrée enregistrée.", "success") flash("Entrée enregistrée.", "success")

View File

@@ -1,19 +1,30 @@
from flask import Blueprint, render_template, request
from datetime import date
from collections import defaultdict from collections import defaultdict
from datetime import date
import sqlalchemy as sa import sqlalchemy as sa
from flask import Blueprint, render_template, request
from app import db from app import db
from app.business.time_calc import count_day_types, minutes_to_str, monthly_stats
from app.business.travel_calc import compute_co2_grams, compute_frais_reels, compute_km_for_entry
from app.config_loader import get_bareme, get_journeys, get_vehicles
from app.models import WorkEntry from app.models import WorkEntry
from app.business.travel_calc import compute_km_for_entry, compute_co2_grams, compute_frais_reels
from app.business.time_calc import count_day_types, monthly_stats, minutes_to_str
from app.config_loader import get_vehicles, get_journeys, get_bareme
bp = Blueprint("reports", __name__, url_prefix="/reports") bp = Blueprint("reports", __name__, url_prefix="/reports")
MONTHS_FR = { MONTHS_FR = {
1: "Janvier", 2: "Février", 3: "Mars", 4: "Avril", 1: "Janvier",
5: "Mai", 6: "Juin", 7: "Juillet", 8: "Août", 2: "Février",
9: "Septembre", 10: "Octobre", 11: "Novembre", 12: "Décembre", 3: "Mars",
4: "Avril",
5: "Mai",
6: "Juin",
7: "Juillet",
8: "Août",
9: "Septembre",
10: "Octobre",
11: "Novembre",
12: "Décembre",
} }
@@ -73,7 +84,9 @@ def index():
"km_by_vehicle": month_km, "km_by_vehicle": month_km,
"km_total": sum(month_km.values()), "km_total": sum(month_km.values()),
"median_daily_str": minutes_to_str(stats["median_daily_min"]) if month_entries else "", "median_daily_str": minutes_to_str(stats["median_daily_min"]) if month_entries else "",
"median_weekly_str": minutes_to_str(stats["median_weekly_min"]) if month_entries else "", "median_weekly_str": minutes_to_str(stats["median_weekly_min"])
if month_entries
else "",
} }
return render_template( return render_template(

View File

@@ -27,13 +27,10 @@ import sqlalchemy as sa
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from app import create_app, db from app import create_app, db
from app.models import WorkEntry, TimeSlot
from app.config_loader import day_types_without_journey, journey_has_motor from app.config_loader import day_types_without_journey, journey_has_motor
from app.models import TimeSlot, WorkEntry
DAY_TYPES = {"WORK", "TT", "GARDE", "ASTREINTE", "FORMATION", "RTT", "CONGE", "MALADE", "FERIE"}
DAY_TYPES = {
"WORK", "TT", "GARDE", "ASTREINTE", "FORMATION", "RTT", "CONGE", "MALADE", "FERIE"
}
def main(csv_path: str, config_path: str = None): def main(csv_path: str, config_path: str = None):
@@ -85,10 +82,10 @@ def main(csv_path: str, config_path: str = None):
if existing: if existing:
# Vérifier si les données sont différentes # Vérifier si les données sont différentes
is_different = ( is_different = (
existing.day_type != day_type or existing.day_type != day_type
existing.journey_profile_id != journey_profile_id or or existing.journey_profile_id != journey_profile_id
existing.motor_vehicle_id != motor_vehicle_id or or existing.motor_vehicle_id != motor_vehicle_id
existing.comment != comment or existing.comment != comment
) )
if is_different: if is_different:
conflicts.append( conflicts.append(
@@ -116,13 +113,17 @@ def main(csv_path: str, config_path: str = None):
e = e.strip() e = e.strip()
if s and e: if s and e:
try: try:
db.session.add(TimeSlot( db.session.add(
entry=entry, TimeSlot(
start_time=time.fromisoformat(s), entry=entry,
end_time=time.fromisoformat(e), start_time=time.fromisoformat(s),
)) end_time=time.fromisoformat(e),
)
)
except (ValueError, AttributeError): except (ValueError, AttributeError):
conflicts.append(f"Ligne {row_num}: format d'heure invalide '{s}' ou '{e}'") conflicts.append(
f"Ligne {row_num}: format d'heure invalide '{s}' ou '{e}'"
)
db.session.rollback() db.session.rollback()
break break
else: else:
@@ -135,7 +136,7 @@ def main(csv_path: str, config_path: str = None):
# Valider et commiter # Valider et commiter
try: try:
db.session.commit() db.session.commit()
except Exception as e: except sa.exc.SQLAlchemyError as e:
db.session.rollback() db.session.rollback()
print(f"Erreur lors du commit: {e}", file=sys.stderr) print(f"Erreur lors du commit: {e}", file=sys.stderr)
return 1 return 1
@@ -156,7 +157,9 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Importer un fichier CSV dans la base de données") parser = argparse.ArgumentParser(description="Importer un fichier CSV dans la base de données")
parser.add_argument("csv_file", help="Chemin vers le fichier CSV à importer") parser.add_argument("csv_file", help="Chemin vers le fichier CSV à importer")
parser.add_argument("--config", default=None, help="Chemin vers le fichier config.toml (optionnel)") parser.add_argument(
"--config", default=None, help="Chemin vers le fichier config.toml (optionnel)"
)
args = parser.parse_args() args = parser.parse_args()

View File

@@ -1,11 +1,14 @@
import pytest import pytest
from app import create_app, db as _db
from app import create_app
from app import db as _db
@pytest.fixture @pytest.fixture
def app(tmp_path): def app(tmp_path):
config_path = tmp_path / "config.toml" config_path = tmp_path / "config.toml"
config_path.write_text(""" config_path.write_text(
"""
[vehicles.citadine] [vehicles.citadine]
name = "Citadine électrique" name = "Citadine électrique"
fuel = "electric" fuel = "electric"
@@ -59,7 +62,9 @@ forfait = 699
km_max = 0 km_max = 0
taux = 0.364 taux = 0.364
forfait = 0 forfait = 0
""", encoding="utf-8") """,
encoding="utf-8",
)
application = create_app(config_path=str(config_path)) application = create_app(config_path=str(config_path))
application.config["TESTING"] = True application.config["TESTING"] = True

View File

@@ -1,6 +1,7 @@
def test_get_vehicles_returns_configured_vehicles(app): def test_get_vehicles_returns_configured_vehicles(app):
with app.app_context(): with app.app_context():
from app.config_loader import get_vehicles from app.config_loader import get_vehicles
vehicles = get_vehicles() vehicles = get_vehicles()
assert "familiale" in vehicles assert "familiale" in vehicles
assert vehicles["familiale"]["co2_per_km"] == 142 assert vehicles["familiale"]["co2_per_km"] == 142
@@ -9,6 +10,7 @@ def test_get_vehicles_returns_configured_vehicles(app):
def test_get_motor_vehicles_excludes_velo(app): def test_get_motor_vehicles_excludes_velo(app):
with app.app_context(): with app.app_context():
from app.config_loader import get_motor_vehicles from app.config_loader import get_motor_vehicles
motor = get_motor_vehicles() motor = get_motor_vehicles()
assert "familiale" in motor assert "familiale" in motor
assert "citadine" in motor assert "citadine" in motor
@@ -19,6 +21,7 @@ def test_get_motor_vehicles_excludes_velo(app):
def test_get_journeys_returns_profiles(app): def test_get_journeys_returns_profiles(app):
with app.app_context(): with app.app_context():
from app.config_loader import get_journeys from app.config_loader import get_journeys
journeys = get_journeys() journeys = get_journeys()
assert "moteur_seul" in journeys assert "moteur_seul" in journeys
assert journeys["moteur_seul"]["distances"]["moteur"] == 25 assert journeys["moteur_seul"]["distances"]["moteur"] == 25
@@ -27,6 +30,7 @@ def test_get_journeys_returns_profiles(app):
def test_journey_has_motor_true(app): def test_journey_has_motor_true(app):
with app.app_context(): with app.app_context():
from app.config_loader import journey_has_motor from app.config_loader import journey_has_motor
assert journey_has_motor("moteur_seul") is True assert journey_has_motor("moteur_seul") is True
assert journey_has_motor("moteur_velo") is True assert journey_has_motor("moteur_velo") is True
@@ -34,6 +38,7 @@ def test_journey_has_motor_true(app):
def test_journey_has_motor_false(app): def test_journey_has_motor_false(app):
with app.app_context(): with app.app_context():
from app.config_loader import journey_has_motor from app.config_loader import journey_has_motor
assert journey_has_motor("velo_seul") is False assert journey_has_motor("velo_seul") is False
assert journey_has_motor(None) is False assert journey_has_motor(None) is False
@@ -41,6 +46,7 @@ def test_journey_has_motor_false(app):
def test_get_bareme_returns_tranches(app): def test_get_bareme_returns_tranches(app):
with app.app_context(): with app.app_context():
from app.config_loader import get_bareme from app.config_loader import get_bareme
tranches = get_bareme(2025, 5) tranches = get_bareme(2025, 5)
assert len(tranches) == 3 assert len(tranches) == 3
assert tranches[0]["taux"] == 0.548 assert tranches[0]["taux"] == 0.548
@@ -49,6 +55,7 @@ def test_get_bareme_returns_tranches(app):
def test_day_types_without_journey(app): def test_day_types_without_journey(app):
with app.app_context(): with app.app_context():
from app.config_loader import day_types_without_journey from app.config_loader import day_types_without_journey
types = day_types_without_journey() types = day_types_without_journey()
assert "TT" in types assert "TT" in types
assert "WORK" not in types assert "WORK" not in types

View File

@@ -1,8 +1,8 @@
from app.business.leave_calc import compute_leave_used, get_or_create_balance
from app.models import WorkEntry, LeaveBalance
from app import db
from datetime import date from datetime import date
import sqlalchemy as sa
from app import db
from app.business.leave_calc import compute_leave_used, get_or_create_balance
from app.models import LeaveBalance, WorkEntry
def test_compute_leave_used_conges(app): def test_compute_leave_used_conges(app):

View File

@@ -1,8 +1,10 @@
from app.models import WorkEntry, TimeSlot from datetime import date
from app import db
from datetime import date, time
import sqlalchemy as sa import sqlalchemy as sa
from app import db
from app.models import WorkEntry
def test_dashboard_empty(client): def test_dashboard_empty(client):
response = client.get("/") response = client.get("/")
@@ -17,21 +19,23 @@ def test_entry_form_get(client):
def test_create_entry(client, app): def test_create_entry(client, app):
response = client.post("/entries/new", data={ response = client.post(
"date": "2025-06-02", "/entries/new",
"day_type": "WORK", data={
"journey_profile_id": "moteur_seul", "date": "2025-06-02",
"motor_vehicle_id": "familiale", "day_type": "WORK",
"start_time": ["09:00"], "journey_profile_id": "moteur_seul",
"end_time": ["17:45"], "motor_vehicle_id": "familiale",
"comment": "", "start_time": ["09:00"],
}, follow_redirects=True) "end_time": ["17:45"],
"comment": "",
},
follow_redirects=True,
)
assert response.status_code == 200 assert response.status_code == 200
with app.app_context(): with app.app_context():
entry = db.session.scalar( entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 2)))
sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 2))
)
assert entry is not None assert entry is not None
assert entry.day_type == "WORK" assert entry.day_type == "WORK"
assert len(entry.time_slots) == 1 assert len(entry.time_slots) == 1
@@ -66,29 +70,29 @@ def test_delete_entry(client, app):
assert response.status_code == 200 assert response.status_code == 200
with app.app_context(): with app.app_context():
deleted = db.session.scalar( deleted = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.id == entry_id))
sa.select(WorkEntry).where(WorkEntry.id == entry_id)
)
assert deleted is None assert deleted is None
def test_create_entry_velo_no_motor_vehicle(client, app): def test_create_entry_velo_no_motor_vehicle(client, app):
"""Un trajet vélo seul ne doit pas enregistrer de motor_vehicle_id.""" """Un trajet vélo seul ne doit pas enregistrer de motor_vehicle_id."""
response = client.post("/entries/new", data={ response = client.post(
"date": "2025-06-10", "/entries/new",
"day_type": "WORK", data={
"journey_profile_id": "velo_seul", "date": "2025-06-10",
"motor_vehicle_id": "", "day_type": "WORK",
"start_time": ["08:30"], "journey_profile_id": "velo_seul",
"end_time": ["17:00"], "motor_vehicle_id": "",
"comment": "", "start_time": ["08:30"],
}, follow_redirects=True) "end_time": ["17:00"],
"comment": "",
},
follow_redirects=True,
)
assert response.status_code == 200 assert response.status_code == 200
with app.app_context(): with app.app_context():
entry = db.session.scalar( entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 10)))
sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 10))
)
assert entry is not None assert entry is not None
assert entry.motor_vehicle_id is None assert entry.motor_vehicle_id is None

View File

@@ -1,7 +1,7 @@
from app.business.time_calc import ( from app.business.time_calc import (
minutes_to_str, minutes_to_str,
work_minutes_reference,
week_balance_minutes, week_balance_minutes,
work_minutes_reference,
) )
@@ -42,10 +42,11 @@ def test_week_balance_negative():
assert week_balance_minutes(2200, 2325) == -125 assert week_balance_minutes(2200, 2325) == -125
from app.business.time_calc import count_day_types from datetime import date
from app.models import WorkEntry, TimeSlot from datetime import time as dtime
from app.business.time_calc import monthly_stats
from datetime import date, time as dtime from app.business.time_calc import count_day_types, monthly_stats
from app.models import TimeSlot, WorkEntry
def test_count_day_types_basic(): def test_count_day_types_basic():
@@ -67,8 +68,10 @@ def _entry(d: date, *slots: tuple[str, str]) -> WorkEntry:
"""Helper : crée un WorkEntry avec des TimeSlots.""" """Helper : crée un WorkEntry avec des TimeSlots."""
entry = WorkEntry(date=d, day_type="WORK") entry = WorkEntry(date=d, day_type="WORK")
entry.time_slots = [ entry.time_slots = [
TimeSlot(start_time=dtime(*[int(x) for x in s.split(":")]), TimeSlot(
end_time=dtime(*[int(x) for x in e.split(":")])) start_time=dtime(*[int(x) for x in s.split(":")]),
end_time=dtime(*[int(x) for x in e.split(":")]),
)
for s, e in slots for s, e in slots
] ]
return entry return entry
@@ -89,9 +92,9 @@ def test_monthly_stats_empty():
def test_monthly_stats_median_daily_odd(): def test_monthly_stats_median_daily_odd():
# 420, 465, 510 → médiane = 465 # 420, 465, 510 → médiane = 465
entries = [ entries = [
_entry(date(2025, 1, 6), ("9:00", "16:00")), # 420 min _entry(date(2025, 1, 6), ("9:00", "16:00")), # 420 min
_entry(date(2025, 1, 7), ("9:00", "16:45")), # 465 min _entry(date(2025, 1, 7), ("9:00", "16:45")), # 465 min
_entry(date(2025, 1, 8), ("9:00", "17:30")), # 510 min _entry(date(2025, 1, 8), ("9:00", "17:30")), # 510 min
] ]
result = monthly_stats(entries) result = monthly_stats(entries)
assert result["median_daily_min"] == 465 assert result["median_daily_min"] == 465
@@ -114,7 +117,7 @@ def test_monthly_stats_median_weekly():
entries = [ entries = [
_entry(date(2025, 1, 6), ("9:00", "16:45")), # sem 2 _entry(date(2025, 1, 6), ("9:00", "16:45")), # sem 2
_entry(date(2025, 1, 7), ("9:00", "16:45")), # sem 2 _entry(date(2025, 1, 7), ("9:00", "16:45")), # sem 2
_entry(date(2025, 1, 13), ("9:00", "16:00")), # sem 3 _entry(date(2025, 1, 13), ("9:00", "16:00")), # sem 3
] ]
result = monthly_stats(entries) result = monthly_stats(entries)
assert result["median_weekly_min"] == 675 assert result["median_weekly_min"] == 675

View File

@@ -1,7 +1,7 @@
from app.business.travel_calc import ( from app.business.travel_calc import (
compute_km_for_entry,
compute_co2_grams, compute_co2_grams,
compute_frais_reels, compute_frais_reels,
compute_km_for_entry,
) )
VEHICLES = { VEHICLES = {
@@ -17,15 +17,15 @@ JOURNEYS = {
} }
TRANCHES_CV3 = [ TRANCHES_CV3 = [
{"km_max": 5000, "taux": 0.529, "forfait": 0}, {"km_max": 5000, "taux": 0.529, "forfait": 0},
{"km_max": 20000, "taux": 0.316, "forfait": 1065}, {"km_max": 20000, "taux": 0.316, "forfait": 1065},
{"km_max": 0, "taux": 0.370, "forfait": 0}, {"km_max": 0, "taux": 0.370, "forfait": 0},
] ]
TRANCHES_CV5 = [ TRANCHES_CV5 = [
{"km_max": 5000, "taux": 0.636, "forfait": 0}, {"km_max": 5000, "taux": 0.636, "forfait": 0},
{"km_max": 20000, "taux": 0.357, "forfait": 1395}, {"km_max": 20000, "taux": 0.357, "forfait": 1395},
{"km_max": 0, "taux": 0.427, "forfait": 0}, {"km_max": 0, "taux": 0.427, "forfait": 0},
] ]