diff --git a/app_new/planning/templates/planning/time_tracking.html b/app_new/planning/templates/planning/time_tracking.html index 128ab12..78467d0 100644 --- a/app_new/planning/templates/planning/time_tracking.html +++ b/app_new/planning/templates/planning/time_tracking.html @@ -14,6 +14,8 @@
Solde réalisé / dû

{{ '+' if variance_minutes >= 0 else '' }}{{ '%d h %02d'|format((variance_minutes|abs) // 60, (variance_minutes|abs) % 60) }}

{% if variance_minutes >= 0 %}heures à récupérer{% else %}heures à faire{% endif %}
+
Récapitulatif mensuelLes heures de travail prévues excluent les permanences, affichées séparément.
{% for item in monthly_summary %}{% else %}{% endfor %}
MoisTravail prévuPermanencesRetardsHeures supplémentairesFormations
{{ months[item.month.month] }} {{ item.month.year }}{{ '%d h %02d'|format(item.planned // 60, item.planned % 60) }}{{ '%d h %02d'|format(item.permanence // 60, item.permanence % 60) }}{{ '%d h %02d'|format(item.retard // 60, item.retard % 60) }}{{ '%d h %02d'|format(item.supplementaire // 60, item.supplementaire % 60) }}{{ '%d h %02d'|format(item.formation // 60, item.formation % 60) }}
Aucune donnée pour cette période.
+
Objectif annuel {{ year }}
@@ -36,7 +38,10 @@
Importer les calendriers
Changer de zone remplace les périodes scolaires importées pour l’autre zone.
-
Permanences dans les vacancesSélectionnez plusieurs dates puis l’horaire type à appliquer.
{% for option in permanence_options %}
{% else %}
Aucune période de vacances importée pour cette période. Importez d’abord le calendrier scolaire.
{% endfor %}
+
Permanences dans les vacancesSélectionnez plusieurs dates puis l’horaire type à appliquer.
{% for option in permanence_options %}
{% else %}
Aucune période de vacances importée pour cette période. Importez d’abord le calendrier scolaire.
{% endfor %}
+ {% set selected_permanences = permanence_options|selectattr('entry')|list %} +
Récapitulatif des permanences
+ {% if selected_permanences %}
{% for option in selected_permanences %}{% endfor %}
DateVacancesHoraire typeHoraires détaillés
{{ 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 %}
{% else %}

Aucune permanence enregistrée pour cette période.

{% endif %}
Saisir un retard ou une heure supplémentaire
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, )