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:
@@ -1,8 +1,9 @@
|
||||
import os
|
||||
|
||||
import sqlalchemy as sa
|
||||
import tomllib
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
import tomllib
|
||||
import os
|
||||
import sqlalchemy as sa
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
@@ -10,6 +11,7 @@ db = SQLAlchemy()
|
||||
def _migrate_db(app):
|
||||
"""Applique les migrations de schéma manquantes (pas d'Alembic)."""
|
||||
import sqlite3
|
||||
|
||||
db_path = os.path.join(app.instance_path, "worklog.db")
|
||||
if not os.path.exists(db_path):
|
||||
return # Nouvelle DB, create_all() s'en charge
|
||||
@@ -25,27 +27,40 @@ def _migrate_db(app):
|
||||
engine = db.engine
|
||||
if "motor_vehicle_id" not in columns:
|
||||
with engine.connect() as conn:
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE work_entries ADD COLUMN motor_vehicle_id VARCHAR(64)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text("ALTER TABLE work_entries ADD COLUMN motor_vehicle_id VARCHAR(64)")
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
_JOURS_FR = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
|
||||
_MOIS_FR = ["", "janvier", "février", "mars", "avril", "mai", "juin",
|
||||
"juillet", "août", "septembre", "octobre", "novembre", "décembre"]
|
||||
_MOIS_FR = [
|
||||
"",
|
||||
"janvier",
|
||||
"février",
|
||||
"mars",
|
||||
"avril",
|
||||
"mai",
|
||||
"juin",
|
||||
"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é",
|
||||
"WORK": "Travail",
|
||||
"TT": "Télétravail",
|
||||
"GARDE": "Garde",
|
||||
"ASTREINTE": "Astreinte",
|
||||
"FORMATION": "Formation",
|
||||
"RTT": "RTT",
|
||||
"CONGE": "Congé",
|
||||
"MALADE": "Maladie",
|
||||
"FERIE": "Férié",
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +70,6 @@ def _day_type_fr(code):
|
||||
|
||||
def _date_fr(d):
|
||||
"""Formate une date en français : 'mercredi 11 mars 2026'."""
|
||||
from datetime import date as date_type
|
||||
jour = _JOURS_FR[d.weekday()]
|
||||
mois = _MOIS_FR[d.month]
|
||||
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)
|
||||
|
||||
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["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.entries import bp as entries_bp
|
||||
from app.routes.reports import bp as reports_bp
|
||||
|
||||
app.register_blueprint(dashboard_bp)
|
||||
app.register_blueprint(entries_bp)
|
||||
app.register_blueprint(reports_bp)
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
from app import db
|
||||
from app.models import WorkEntry, LeaveBalance
|
||||
import sqlalchemy as sa
|
||||
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]:
|
||||
start = date(year, 1, 1)
|
||||
end = date(year, 12, 31)
|
||||
|
||||
conges = db.session.scalar(
|
||||
sa.select(sa.func.count()).where(
|
||||
WorkEntry.date.between(start, end),
|
||||
WorkEntry.day_type == "CONGE",
|
||||
conges = (
|
||||
db.session.scalar(
|
||||
sa.select(sa.func.count()).where(
|
||||
WorkEntry.date.between(start, end),
|
||||
WorkEntry.day_type == "CONGE",
|
||||
)
|
||||
)
|
||||
) or 0
|
||||
or 0
|
||||
)
|
||||
|
||||
rtt = db.session.scalar(
|
||||
sa.select(sa.func.count()).where(
|
||||
WorkEntry.date.between(start, end),
|
||||
WorkEntry.day_type == "RTT",
|
||||
rtt = (
|
||||
db.session.scalar(
|
||||
sa.select(sa.func.count()).where(
|
||||
WorkEntry.date.between(start, end),
|
||||
WorkEntry.day_type == "RTT",
|
||||
)
|
||||
)
|
||||
) or 0
|
||||
or 0
|
||||
)
|
||||
|
||||
return {"conges": conges, "rtt": rtt}
|
||||
|
||||
|
||||
def get_or_create_balance(year: int) -> LeaveBalance:
|
||||
balance = db.session.scalar(
|
||||
sa.select(LeaveBalance).where(LeaveBalance.year == year)
|
||||
)
|
||||
balance = db.session.scalar(sa.select(LeaveBalance).where(LeaveBalance.year == year))
|
||||
if balance is None:
|
||||
balance = LeaveBalance(year=year)
|
||||
db.session.add(balance)
|
||||
|
||||
@@ -32,7 +32,9 @@ def compute_co2_grams(km_by_vehicle: dict[str, int], vehicles: dict) -> float:
|
||||
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.
|
||||
km_max = 0 signifie "pas de limite" (dernière tranche).
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from app import db
|
||||
from datetime import date, time, datetime
|
||||
from datetime import date, datetime, time
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as so
|
||||
|
||||
from app import db
|
||||
|
||||
|
||||
class WorkEntry(db.Model):
|
||||
__tablename__ = "work_entries"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from flask import Blueprint, render_template
|
||||
from datetime import date, timedelta
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
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.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__)
|
||||
|
||||
@@ -45,9 +47,7 @@ def index():
|
||||
balance = get_or_create_balance(year)
|
||||
used = compute_leave_used(year)
|
||||
|
||||
today_entry = db.session.scalar(
|
||||
sa.select(WorkEntry).where(WorkEntry.date == today)
|
||||
)
|
||||
today_entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == today))
|
||||
|
||||
return render_template(
|
||||
"dashboard.html",
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from datetime import date, time
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
|
||||
from app import db
|
||||
from app.models import WorkEntry, TimeSlot
|
||||
from app.config_loader import get_journeys, get_motor_vehicles, day_types_without_journey, journey_has_motor
|
||||
from app.config_loader import (
|
||||
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")
|
||||
|
||||
@@ -22,9 +29,7 @@ DAY_TYPES = [
|
||||
|
||||
@bp.route("/")
|
||||
def list_entries():
|
||||
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)
|
||||
|
||||
|
||||
@@ -51,9 +56,7 @@ def entry_form(entry_id=None):
|
||||
journey_profile_id = None
|
||||
|
||||
if entry is None:
|
||||
existing = db.session.scalar(
|
||||
sa.select(WorkEntry).where(WorkEntry.date == entry_date)
|
||||
)
|
||||
existing = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == entry_date))
|
||||
if existing:
|
||||
flash(f"Une entrée existe déjà pour le {entry_date}.", "error")
|
||||
return redirect(url_for("entries.entry_form"))
|
||||
@@ -72,11 +75,13 @@ def entry_form(entry_id=None):
|
||||
ends = request.form.getlist("end_time")
|
||||
for s, e in zip(starts, ends):
|
||||
if s and e:
|
||||
db.session.add(TimeSlot(
|
||||
entry=entry,
|
||||
start_time=time.fromisoformat(s),
|
||||
end_time=time.fromisoformat(e),
|
||||
))
|
||||
db.session.add(
|
||||
TimeSlot(
|
||||
entry=entry,
|
||||
start_time=time.fromisoformat(s),
|
||||
end_time=time.fromisoformat(e),
|
||||
)
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
flash("Entrée enregistrée.", "success")
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
from flask import Blueprint, render_template, request
|
||||
from datetime import date
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import Blueprint, render_template, request
|
||||
|
||||
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.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")
|
||||
|
||||
MONTHS_FR = {
|
||||
1: "Janvier", 2: "Février", 3: "Mars", 4: "Avril",
|
||||
5: "Mai", 6: "Juin", 7: "Juillet", 8: "Août",
|
||||
9: "Septembre", 10: "Octobre", 11: "Novembre", 12: "Décembre",
|
||||
1: "Janvier",
|
||||
2: "Février",
|
||||
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_total": sum(month_km.values()),
|
||||
"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(
|
||||
|
||||
@@ -27,29 +27,26 @@ import sqlalchemy as sa
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
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.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):
|
||||
"""Importe les données depuis un fichier CSV vers la base de données."""
|
||||
|
||||
|
||||
# Créer l'application Flask avec la config
|
||||
app = create_app(config_path=config_path)
|
||||
|
||||
|
||||
with app.app_context():
|
||||
conflicts = []
|
||||
imported_count = 0
|
||||
|
||||
|
||||
# Lire le fichier CSV
|
||||
with open(csv_path, "r", encoding="utf-8") as f:
|
||||
csv_reader = csv.DictReader(f)
|
||||
|
||||
|
||||
for row_num, row in enumerate(csv_reader, start=2):
|
||||
try:
|
||||
# Parser la date
|
||||
@@ -57,38 +54,38 @@ def main(csv_path: str, config_path: str = None):
|
||||
except (ValueError, AttributeError):
|
||||
conflicts.append(f"Ligne {row_num}: date invalide ou manquante")
|
||||
continue
|
||||
|
||||
|
||||
# Valider le type de jour
|
||||
day_type = row.get("day_type", "WORK").strip().upper()
|
||||
if day_type not in DAY_TYPES:
|
||||
conflicts.append(f"Ligne {row_num}: type de jour invalide '{day_type}'")
|
||||
continue
|
||||
|
||||
|
||||
# Récupérer les autres champs
|
||||
journey_profile_id = row.get("journey_profile_id", "").strip() or None
|
||||
motor_vehicle_id = row.get("motor_vehicle_id", "").strip() or None
|
||||
comment = row.get("comment", "").strip() or None
|
||||
|
||||
|
||||
# Si le type de jour n'a pas de trajet, forcer journey_profile_id à None
|
||||
if day_type in day_types_without_journey():
|
||||
journey_profile_id = None
|
||||
|
||||
|
||||
# Si le profil de trajet n'a pas de moteur, forcer motor_vehicle_id à None
|
||||
if journey_profile_id and not journey_has_motor(journey_profile_id):
|
||||
motor_vehicle_id = None
|
||||
|
||||
|
||||
# Vérifier si une entrée existe déjà pour cette date
|
||||
existing = db.session.scalar(
|
||||
sa.select(WorkEntry).where(WorkEntry.date == entry_date)
|
||||
)
|
||||
|
||||
|
||||
if existing:
|
||||
# Vérifier si les données sont différentes
|
||||
is_different = (
|
||||
existing.day_type != day_type or
|
||||
existing.journey_profile_id != journey_profile_id or
|
||||
existing.motor_vehicle_id != motor_vehicle_id or
|
||||
existing.comment != comment
|
||||
existing.day_type != day_type
|
||||
or existing.journey_profile_id != journey_profile_id
|
||||
or existing.motor_vehicle_id != motor_vehicle_id
|
||||
or existing.comment != comment
|
||||
)
|
||||
if is_different:
|
||||
conflicts.append(
|
||||
@@ -96,7 +93,7 @@ def main(csv_path: str, config_path: str = None):
|
||||
f"Données existantes conservées."
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
# Créer la nouvelle entrée
|
||||
entry = WorkEntry(
|
||||
date=entry_date,
|
||||
@@ -106,58 +103,64 @@ def main(csv_path: str, config_path: str = None):
|
||||
comment=comment,
|
||||
)
|
||||
db.session.add(entry)
|
||||
|
||||
|
||||
# Ajouter les plages horaires
|
||||
start_times = row.get("start_time", "").split(";")
|
||||
end_times = row.get("end_time", "").split(";")
|
||||
|
||||
|
||||
for s, e in zip(start_times, end_times):
|
||||
s = s.strip()
|
||||
e = e.strip()
|
||||
if s and e:
|
||||
try:
|
||||
db.session.add(TimeSlot(
|
||||
entry=entry,
|
||||
start_time=time.fromisoformat(s),
|
||||
end_time=time.fromisoformat(e),
|
||||
))
|
||||
db.session.add(
|
||||
TimeSlot(
|
||||
entry=entry,
|
||||
start_time=time.fromisoformat(s),
|
||||
end_time=time.fromisoformat(e),
|
||||
)
|
||||
)
|
||||
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()
|
||||
break
|
||||
else:
|
||||
imported_count += 1
|
||||
continue
|
||||
|
||||
|
||||
db.session.rollback()
|
||||
break
|
||||
|
||||
|
||||
# Valider et commiter
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
except sa.exc.SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
print(f"Erreur lors du commit: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
# Afficher les résultats
|
||||
print(f"Import terminé: {imported_count} entrée(s) importée(s)")
|
||||
|
||||
|
||||
if conflicts:
|
||||
print(f"\n⚠️ {len(conflicts)} avertissement(s):")
|
||||
for conflict in conflicts:
|
||||
print(f" - {conflict}")
|
||||
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
|
||||
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("--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()
|
||||
|
||||
|
||||
sys.exit(main(args.csv_file, args.config))
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import pytest
|
||||
from app import create_app, db as _db
|
||||
|
||||
from app import create_app
|
||||
from app import db as _db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(tmp_path):
|
||||
config_path = tmp_path / "config.toml"
|
||||
config_path.write_text("""
|
||||
config_path.write_text(
|
||||
"""
|
||||
[vehicles.citadine]
|
||||
name = "Citadine électrique"
|
||||
fuel = "electric"
|
||||
@@ -59,7 +62,9 @@ forfait = 699
|
||||
km_max = 0
|
||||
taux = 0.364
|
||||
forfait = 0
|
||||
""", encoding="utf-8")
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
application = create_app(config_path=str(config_path))
|
||||
application.config["TESTING"] = True
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
def test_get_vehicles_returns_configured_vehicles(app):
|
||||
with app.app_context():
|
||||
from app.config_loader import get_vehicles
|
||||
|
||||
vehicles = get_vehicles()
|
||||
assert "familiale" in vehicles
|
||||
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):
|
||||
with app.app_context():
|
||||
from app.config_loader import get_motor_vehicles
|
||||
|
||||
motor = get_motor_vehicles()
|
||||
assert "familiale" 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):
|
||||
with app.app_context():
|
||||
from app.config_loader import get_journeys
|
||||
|
||||
journeys = get_journeys()
|
||||
assert "moteur_seul" in journeys
|
||||
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):
|
||||
with app.app_context():
|
||||
from app.config_loader import journey_has_motor
|
||||
|
||||
assert journey_has_motor("moteur_seul") 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):
|
||||
with app.app_context():
|
||||
from app.config_loader import journey_has_motor
|
||||
|
||||
assert journey_has_motor("velo_seul") 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):
|
||||
with app.app_context():
|
||||
from app.config_loader import get_bareme
|
||||
|
||||
tranches = get_bareme(2025, 5)
|
||||
assert len(tranches) == 3
|
||||
assert tranches[0]["taux"] == 0.548
|
||||
@@ -49,6 +55,7 @@ def test_get_bareme_returns_tranches(app):
|
||||
def test_day_types_without_journey(app):
|
||||
with app.app_context():
|
||||
from app.config_loader import day_types_without_journey
|
||||
|
||||
types = day_types_without_journey()
|
||||
assert "TT" in types
|
||||
assert "WORK" not in types
|
||||
|
||||
@@ -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
|
||||
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):
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from app.models import WorkEntry, TimeSlot
|
||||
from app import db
|
||||
from datetime import date, time
|
||||
from datetime import date
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.models import WorkEntry
|
||||
|
||||
|
||||
def test_dashboard_empty(client):
|
||||
response = client.get("/")
|
||||
@@ -17,21 +19,23 @@ def test_entry_form_get(client):
|
||||
|
||||
|
||||
def test_create_entry(client, app):
|
||||
response = client.post("/entries/new", data={
|
||||
"date": "2025-06-02",
|
||||
"day_type": "WORK",
|
||||
"journey_profile_id": "moteur_seul",
|
||||
"motor_vehicle_id": "familiale",
|
||||
"start_time": ["09:00"],
|
||||
"end_time": ["17:45"],
|
||||
"comment": "",
|
||||
}, follow_redirects=True)
|
||||
response = client.post(
|
||||
"/entries/new",
|
||||
data={
|
||||
"date": "2025-06-02",
|
||||
"day_type": "WORK",
|
||||
"journey_profile_id": "moteur_seul",
|
||||
"motor_vehicle_id": "familiale",
|
||||
"start_time": ["09:00"],
|
||||
"end_time": ["17:45"],
|
||||
"comment": "",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
with app.app_context():
|
||||
entry = db.session.scalar(
|
||||
sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 2))
|
||||
)
|
||||
entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 2)))
|
||||
assert entry is not None
|
||||
assert entry.day_type == "WORK"
|
||||
assert len(entry.time_slots) == 1
|
||||
@@ -66,29 +70,29 @@ def test_delete_entry(client, app):
|
||||
assert response.status_code == 200
|
||||
|
||||
with app.app_context():
|
||||
deleted = db.session.scalar(
|
||||
sa.select(WorkEntry).where(WorkEntry.id == entry_id)
|
||||
)
|
||||
deleted = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.id == entry_id))
|
||||
assert deleted is None
|
||||
|
||||
|
||||
def test_create_entry_velo_no_motor_vehicle(client, app):
|
||||
"""Un trajet vélo seul ne doit pas enregistrer de motor_vehicle_id."""
|
||||
response = client.post("/entries/new", data={
|
||||
"date": "2025-06-10",
|
||||
"day_type": "WORK",
|
||||
"journey_profile_id": "velo_seul",
|
||||
"motor_vehicle_id": "",
|
||||
"start_time": ["08:30"],
|
||||
"end_time": ["17:00"],
|
||||
"comment": "",
|
||||
}, follow_redirects=True)
|
||||
response = client.post(
|
||||
"/entries/new",
|
||||
data={
|
||||
"date": "2025-06-10",
|
||||
"day_type": "WORK",
|
||||
"journey_profile_id": "velo_seul",
|
||||
"motor_vehicle_id": "",
|
||||
"start_time": ["08:30"],
|
||||
"end_time": ["17:00"],
|
||||
"comment": "",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
with app.app_context():
|
||||
entry = db.session.scalar(
|
||||
sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 10))
|
||||
)
|
||||
entry = db.session.scalar(sa.select(WorkEntry).where(WorkEntry.date == date(2025, 6, 10)))
|
||||
assert entry is not None
|
||||
assert entry.motor_vehicle_id is None
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from app.business.time_calc import (
|
||||
minutes_to_str,
|
||||
work_minutes_reference,
|
||||
week_balance_minutes,
|
||||
work_minutes_reference,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,10 +42,11 @@ def test_week_balance_negative():
|
||||
assert week_balance_minutes(2200, 2325) == -125
|
||||
|
||||
|
||||
from app.business.time_calc import count_day_types
|
||||
from app.models import WorkEntry, TimeSlot
|
||||
from app.business.time_calc import monthly_stats
|
||||
from datetime import date, time as dtime
|
||||
from datetime import date
|
||||
from datetime import 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():
|
||||
@@ -67,8 +68,10 @@ def _entry(d: date, *slots: tuple[str, str]) -> WorkEntry:
|
||||
"""Helper : crée un WorkEntry avec des TimeSlots."""
|
||||
entry = WorkEntry(date=d, day_type="WORK")
|
||||
entry.time_slots = [
|
||||
TimeSlot(start_time=dtime(*[int(x) for x in s.split(":")]),
|
||||
end_time=dtime(*[int(x) for x in e.split(":")]))
|
||||
TimeSlot(
|
||||
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
|
||||
]
|
||||
return entry
|
||||
@@ -89,9 +92,9 @@ def test_monthly_stats_empty():
|
||||
def test_monthly_stats_median_daily_odd():
|
||||
# 420, 465, 510 → médiane = 465
|
||||
entries = [
|
||||
_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, 8), ("9:00", "17:30")), # 510 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, 8), ("9:00", "17:30")), # 510 min
|
||||
]
|
||||
result = monthly_stats(entries)
|
||||
assert result["median_daily_min"] == 465
|
||||
@@ -114,7 +117,7 @@ def test_monthly_stats_median_weekly():
|
||||
entries = [
|
||||
_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, 13), ("9:00", "16:00")), # sem 3
|
||||
_entry(date(2025, 1, 13), ("9:00", "16:00")), # sem 3
|
||||
]
|
||||
result = monthly_stats(entries)
|
||||
assert result["median_weekly_min"] == 675
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from app.business.travel_calc import (
|
||||
compute_km_for_entry,
|
||||
compute_co2_grams,
|
||||
compute_frais_reels,
|
||||
compute_km_for_entry,
|
||||
)
|
||||
|
||||
VEHICLES = {
|
||||
@@ -17,15 +17,15 @@ JOURNEYS = {
|
||||
}
|
||||
|
||||
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": 0, "taux": 0.370, "forfait": 0},
|
||||
{"km_max": 0, "taux": 0.370, "forfait": 0},
|
||||
]
|
||||
|
||||
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": 0, "taux": 0.427, "forfait": 0},
|
||||
{"km_max": 0, "taux": 0.427, "forfait": 0},
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user