Grille : ordonner les créneaux de nuit après minuit selon la vraie journée
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f565feb3bc
commit
4bcdafca96
@@ -12,10 +12,22 @@ def _dates(edition: Edition) -> list[dt.date]:
|
||||
return [edition.start_date + dt.timedelta(days=i) for i in range(span + 1)]
|
||||
|
||||
|
||||
def _window(day: dt.date, part: DayPart) -> tuple[dt.datetime, dt.datetime]:
|
||||
start = dt.datetime.combine(day, part.start_time)
|
||||
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 = (
|
||||
day if part.end_time > part.start_time else day + dt.timedelta(days=1)
|
||||
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)
|
||||
|
||||
@@ -28,6 +40,7 @@ def generate_missing_shifts(
|
||||
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(
|
||||
@@ -37,9 +50,9 @@ def generate_missing_shifts(
|
||||
created = 0
|
||||
for day in _dates(edition):
|
||||
for part in day_parts:
|
||||
if (part.id, day) in existing:
|
||||
start, end = _window(day, part, boundary)
|
||||
if (part.id, start.date()) in existing:
|
||||
continue
|
||||
start, end = _window(day, part)
|
||||
session.add(
|
||||
Shift(
|
||||
edition_id=edition.id,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Génération des créneaux d'un poste à partir des créneaux types."""
|
||||
|
||||
import datetime as dt
|
||||
|
||||
|
||||
def _positions_url(edition):
|
||||
return f"/editions/{edition.id}/positions"
|
||||
@@ -58,6 +60,31 @@ def test_adding_a_day_part_then_generating_adds_it_everywhere(client, edition):
|
||||
assert resp.json()["created"] == 4 # 1 par jour
|
||||
|
||||
|
||||
def test_day_part_before_the_day_boundary_rolls_to_the_next_calendar_day(client, edition):
|
||||
"""La frontière du jour est le début du créneau type de plus petit `order`
|
||||
(ici MORNING, 06 h — voir CLAUDE.md § 3 et `validation.engine._day_boundary`).
|
||||
Un créneau type qui commence avant cette heure (ex. `Nuit` 02 h–05 h,
|
||||
suite de `Soirée` 19 h–00 h) est donc la fin de la nuit du jour
|
||||
*précédent* : il doit être généré sur le jour suivant, pas sur `day`."""
|
||||
position = _create_position(client, edition)
|
||||
client.post(
|
||||
f"/editions/{edition.id}/day-parts",
|
||||
json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00", "order": 3},
|
||||
)
|
||||
client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts")
|
||||
|
||||
shifts = client.get(
|
||||
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
|
||||
).json()
|
||||
nuit_shifts = [s for s in shifts if dt.time.fromisoformat(s["starts_at"][11:19]) == dt.time(2, 0)]
|
||||
assert len(nuit_shifts) == 4
|
||||
|
||||
nuit_dates = sorted(dt.date.fromisoformat(s["starts_at"][:10]) for s in nuit_shifts)
|
||||
# Édition du 9 au 12/07 : la nuit du 9 (Soirée 19 h) se termine le 10 à
|
||||
# 05 h, ..., la nuit du 12 se termine le 13 — jamais le 9 lui-même.
|
||||
assert nuit_dates == [dt.date(2026, 7, d) for d in (10, 11, 12, 13)]
|
||||
|
||||
|
||||
def test_generation_does_not_touch_staffing_already_set(client, edition):
|
||||
position = _create_position(client, edition)
|
||||
shift = client.get(
|
||||
|
||||
Reference in New Issue
Block a user