@@ -36,7 +38,10 @@
-
+
+ {% set selected_permanences = permanence_options|selectattr('entry')|list %}
+
Récapitulatif des permanences
+ {% if selected_permanences %}
| Date | Vacances | Horaire type | Horaires détaillés | |
{% for option in selected_permanences %}{{ days.get(option.date.weekday()) }} {{ option.date.strftime('%d/%m/%Y') }} | {{ option.closure.name }} | {{ option.entry.template.name if option.entry.template else 'Horaire personnalisé' }} | {{ option.entry.start_time.strftime('%H:%M') }}–{{ option.entry.end_time.strftime('%H:%M') }}{% if option.entry.lunch_start and option.entry.lunch_end %} Pause {{ option.entry.lunch_start.strftime('%H:%M') }}–{{ option.entry.lunch_end.strftime('%H:%M') }}{% endif %} | |
{% endfor %}
{% else %}
Aucune permanence enregistrée pour cette période.
{% endif %}
diff --git a/app_new/planning/time_tracking.py b/app_new/planning/time_tracking.py
index e614c9d..fd09351 100644
--- a/app_new/planning/time_tracking.py
+++ b/app_new/planning/time_tracking.py
@@ -39,6 +39,16 @@ def _period(year):
return date(year, 9, 1), date(year + 1, 8, 31)
+def _month_starts(start, end):
+ """Retourne les premiers jours de chaque mois de la période comptable."""
+ current = date(start.year, start.month, 1)
+ result = []
+ while current <= end:
+ result.append(current)
+ current = date(current.year + (current.month == 12), 1 if current.month == 12 else current.month + 1, 1)
+ return result
+
+
@planning_bp.route("/time")
@login_required
def time_tracking():
@@ -69,6 +79,14 @@ def time_tracking():
TimeEntry.work_date >= period_start, TimeEntry.work_date <= period_end,
).order_by(TimeEntry.work_date.desc()).all()
planned = 0
+ planned_minutes_by_month = {}
+ permanence_minutes_by_month = {}
+ for work_day in work_days:
+ if period_start <= work_day.work_date <= period_end:
+ month = date(work_day.work_date.year, work_day.work_date.month, 1)
+ permanence_minutes_by_month[month] = permanence_minutes_by_month.get(month, 0) + _minutes(
+ work_day.start_time, work_day.end_time, work_day.lunch_start, work_day.lunch_end
+ )
current = period_start
while current <= period_end:
hours = None
@@ -89,7 +107,10 @@ def time_tracking():
source = assignment.template if assignment.template and assignment.template.is_active else assignment
hours = (source.start_time, source.end_time, source.lunch_start, source.lunch_end)
if hours:
- planned += _minutes(*hours)
+ day_minutes = _minutes(*hours)
+ planned += day_minutes
+ month = date(current.year, current.month, 1)
+ planned_minutes_by_month[month] = planned_minutes_by_month.get(month, 0) + day_minutes
current += timedelta(days=1)
permanence_options = []
for closure in closures:
@@ -103,12 +124,50 @@ def time_tracking():
permanence_options.append({"date": current_day, "closure": closure, "entry": existing})
current_day += timedelta(days=1)
actual = sum(item.actual_minutes or 0 for item in entries)
+ # Synthèse mensuelle : les permanences sont isolées du temps de travail
+ # planifié habituel, et les formations viennent des convocations validées.
+ monthly_summary = []
+ trainings = Training.query.join(TrainingParticipant).filter(
+ TrainingParticipant.user_id == current_user.id,
+ TrainingParticipant.is_confirmed.is_(True),
+ Training.end_date >= period_start,
+ Training.start_date <= period_end,
+ ).all()
+ for month in _month_starts(period_start, period_end):
+ month_end = date(month.year + (month.month == 12), 1 if month.month == 12 else month.month + 1, 1) - timedelta(days=1)
+ month_entries = [item for item in entries if month <= item.work_date <= month_end]
+ training_minutes = 0
+ for training in trainings:
+ start = max(training.start_date or month, month, period_start)
+ end = min(training.end_date or start, month_end, period_end)
+ if end < start:
+ continue
+ day = start
+ while day <= end:
+ if day.weekday() < 5:
+ if training.start_time and training.end_time:
+ training_minutes += _minutes(training.start_time, training.end_time)
+ elif assignments.get(day.weekday()):
+ assignment = assignments[day.weekday()]
+ source = assignment.template if assignment.template and assignment.template.is_active else assignment
+ training_minutes += _minutes(source.start_time, source.end_time, source.lunch_start, source.lunch_end)
+ day += timedelta(days=1)
+ monthly_summary.append({
+ "month": month,
+ "planned": max(planned_minutes_by_month.get(month, 0) - permanence_minutes_by_month.get(month, 0), 0),
+ "permanence": permanence_minutes_by_month.get(month, 0),
+ "retard": sum(item.actual_minutes or 0 for item in month_entries if item.entry_type == "retard"),
+ "supplementaire": sum(item.actual_minutes or 0 for item in month_entries if item.entry_type == "supplementaire"),
+ "formation": training_minutes,
+ })
return render_template(
"planning/time_tracking.html", year=year, period_start=period_start, period_end=period_end, time_config=config, templates=templates,
assignments=assignments, closures=closures, permanence_options=permanence_options, entries=entries,
days={0: "Lundi", 1: "Mardi", 2: "Mercredi", 3: "Jeudi", 4: "Vendredi", 5: "Samedi", 6: "Dimanche"},
+ months={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"},
planned_minutes=planned, actual_minutes=actual,
required_minutes=config.minutes_to_work, variance_minutes=actual - config.minutes_to_work,
+ monthly_summary=monthly_summary,
)