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 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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -27,29 +27,26 @@ 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):
|
||||||
"""Importe les données depuis un fichier CSV vers la base de données."""
|
"""Importe les données depuis un fichier CSV vers la base de données."""
|
||||||
|
|
||||||
# Créer l'application Flask avec la config
|
# Créer l'application Flask avec la config
|
||||||
app = create_app(config_path=config_path)
|
app = create_app(config_path=config_path)
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
conflicts = []
|
conflicts = []
|
||||||
imported_count = 0
|
imported_count = 0
|
||||||
|
|
||||||
# Lire le fichier CSV
|
# Lire le fichier CSV
|
||||||
with open(csv_path, "r", encoding="utf-8") as f:
|
with open(csv_path, "r", encoding="utf-8") as f:
|
||||||
csv_reader = csv.DictReader(f)
|
csv_reader = csv.DictReader(f)
|
||||||
|
|
||||||
for row_num, row in enumerate(csv_reader, start=2):
|
for row_num, row in enumerate(csv_reader, start=2):
|
||||||
try:
|
try:
|
||||||
# Parser la date
|
# Parser la date
|
||||||
@@ -57,38 +54,38 @@ def main(csv_path: str, config_path: str = None):
|
|||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
conflicts.append(f"Ligne {row_num}: date invalide ou manquante")
|
conflicts.append(f"Ligne {row_num}: date invalide ou manquante")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Valider le type de jour
|
# Valider le type de jour
|
||||||
day_type = row.get("day_type", "WORK").strip().upper()
|
day_type = row.get("day_type", "WORK").strip().upper()
|
||||||
if day_type not in DAY_TYPES:
|
if day_type not in DAY_TYPES:
|
||||||
conflicts.append(f"Ligne {row_num}: type de jour invalide '{day_type}'")
|
conflicts.append(f"Ligne {row_num}: type de jour invalide '{day_type}'")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Récupérer les autres champs
|
# Récupérer les autres champs
|
||||||
journey_profile_id = row.get("journey_profile_id", "").strip() or None
|
journey_profile_id = row.get("journey_profile_id", "").strip() or None
|
||||||
motor_vehicle_id = row.get("motor_vehicle_id", "").strip() or None
|
motor_vehicle_id = row.get("motor_vehicle_id", "").strip() or None
|
||||||
comment = row.get("comment", "").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
|
# Si le type de jour n'a pas de trajet, forcer journey_profile_id à None
|
||||||
if day_type in day_types_without_journey():
|
if day_type in day_types_without_journey():
|
||||||
journey_profile_id = None
|
journey_profile_id = None
|
||||||
|
|
||||||
# Si le profil de trajet n'a pas de moteur, forcer motor_vehicle_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):
|
if journey_profile_id and not journey_has_motor(journey_profile_id):
|
||||||
motor_vehicle_id = None
|
motor_vehicle_id = None
|
||||||
|
|
||||||
# Vérifier si une entrée existe déjà pour cette date
|
# Vérifier si une entrée existe déjà pour cette date
|
||||||
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:
|
||||||
# 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(
|
||||||
@@ -96,7 +93,7 @@ def main(csv_path: str, config_path: str = None):
|
|||||||
f"Données existantes conservées."
|
f"Données existantes conservées."
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Créer la nouvelle entrée
|
# Créer la nouvelle entrée
|
||||||
entry = WorkEntry(
|
entry = WorkEntry(
|
||||||
date=entry_date,
|
date=entry_date,
|
||||||
@@ -106,58 +103,64 @@ def main(csv_path: str, config_path: str = None):
|
|||||||
comment=comment,
|
comment=comment,
|
||||||
)
|
)
|
||||||
db.session.add(entry)
|
db.session.add(entry)
|
||||||
|
|
||||||
# Ajouter les plages horaires
|
# Ajouter les plages horaires
|
||||||
start_times = row.get("start_time", "").split(";")
|
start_times = row.get("start_time", "").split(";")
|
||||||
end_times = row.get("end_time", "").split(";")
|
end_times = row.get("end_time", "").split(";")
|
||||||
|
|
||||||
for s, e in zip(start_times, end_times):
|
for s, e in zip(start_times, end_times):
|
||||||
s = s.strip()
|
s = s.strip()
|
||||||
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:
|
||||||
imported_count += 1
|
imported_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
break
|
break
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
# Afficher les résultats
|
# Afficher les résultats
|
||||||
print(f"Import terminé: {imported_count} entrée(s) importée(s)")
|
print(f"Import terminé: {imported_count} entrée(s) importée(s)")
|
||||||
|
|
||||||
if conflicts:
|
if conflicts:
|
||||||
print(f"\n⚠️ {len(conflicts)} avertissement(s):")
|
print(f"\n⚠️ {len(conflicts)} avertissement(s):")
|
||||||
for conflict in conflicts:
|
for conflict in conflicts:
|
||||||
print(f" - {conflict}")
|
print(f" - {conflict}")
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
sys.exit(main(args.csv_file, args.config))
|
sys.exit(main(args.csv_file, args.config))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user