Un créneau type commençant avant la frontière du jour (le plus petit `order`, ex. « Nuit » 00 h–03 h après « Soirée » 19 h–00 h) était généré sur la même date calendaire que la soirée elle-même au lieu du lendemain, et s'affichait donc avant 19 h/22 h/23 h dans la grille des besoins au lieu d'après 23 h. Corrigé dans la génération des créneaux (_shiftgen.py, même frontière que validation.engine._day_boundary) et dans le regroupement/tri de la grille (planning.ts : `dayBoundary` calculé depuis les créneaux types). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
69 lines
2.5 KiB
Python
69 lines
2.5 KiB
Python
"""Génération des créneaux d'un poste à partir des créneaux types (DayPart)."""
|
||
|
||
import datetime as dt
|
||
|
||
from sqlmodel import Session, select
|
||
|
||
from app.models import DayPart, Edition, Position, Shift
|
||
|
||
|
||
def _dates(edition: Edition) -> list[dt.date]:
|
||
span = (edition.end_date - edition.start_date).days
|
||
return [edition.start_date + dt.timedelta(days=i) for i in range(span + 1)]
|
||
|
||
|
||
def _day_boundary(day_parts: list[DayPart]) -> dt.time:
|
||
"""Début du premier créneau type du jour (le plus petit `order`) — même
|
||
frontière que `validation.engine._day_boundary`. Un créneau type dont
|
||
l'heure de début tombe avant cette frontière est la suite de la nuit du
|
||
jour précédent (ex. `Nuit3` 00 h–01 h après `Soirée` 19 h–00 h) : il doit
|
||
être généré sur le jour *suivant*, pas sur `day` lui-même."""
|
||
if not day_parts:
|
||
return dt.time(0, 0)
|
||
return min(day_parts, key=lambda part: part.order).start_time
|
||
|
||
|
||
def _window(day: dt.date, part: DayPart, boundary: dt.time) -> tuple[dt.datetime, dt.datetime]:
|
||
actual_day = day + dt.timedelta(days=1) if part.start_time < boundary else day
|
||
start = dt.datetime.combine(actual_day, part.start_time)
|
||
end_day = (
|
||
actual_day if part.end_time > part.start_time else actual_day + dt.timedelta(days=1)
|
||
)
|
||
return start, dt.datetime.combine(end_day, part.end_time)
|
||
|
||
|
||
def generate_missing_shifts(
|
||
session: Session, edition: Edition, position: Position
|
||
) -> int:
|
||
"""Crée, pour chaque jour de l'édition et chaque créneau type, un créneau à
|
||
besoin 0/0 s'il n'existe pas déjà. Retourne le nombre créé."""
|
||
day_parts = list(
|
||
session.exec(select(DayPart).where(DayPart.edition_id == edition.id))
|
||
)
|
||
boundary = _day_boundary(day_parts)
|
||
existing = {
|
||
(s.day_part_id, s.starts_at.date())
|
||
for s in session.exec(
|
||
select(Shift).where(Shift.position_id == position.id)
|
||
)
|
||
}
|
||
created = 0
|
||
for day in _dates(edition):
|
||
for part in day_parts:
|
||
start, end = _window(day, part, boundary)
|
||
if (part.id, start.date()) in existing:
|
||
continue
|
||
session.add(
|
||
Shift(
|
||
edition_id=edition.id,
|
||
position_id=position.id,
|
||
day_part_id=part.id,
|
||
starts_at=start,
|
||
ends_at=end,
|
||
min_staff=0,
|
||
ideal_staff=0,
|
||
)
|
||
)
|
||
created += 1
|
||
return created
|