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>
100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""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"
|
||
|
||
|
||
def _create_position(client, edition, name="Bar"):
|
||
return client.post(
|
||
_positions_url(edition), json={"name": name, "category": "BAR"}
|
||
).json()
|
||
|
||
|
||
def test_creating_a_position_generates_one_shift_per_day_and_type(client, edition):
|
||
position = _create_position(client, edition)
|
||
shifts = client.get(
|
||
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
|
||
).json()
|
||
# 4 jours (2026-07-09..12) x 3 créneaux types.
|
||
assert len(shifts) == 12
|
||
assert all(s["min_staff"] == 0 and s["ideal_staff"] == 0 for s in shifts)
|
||
assert all(s["day_part_id"] is not None for s in shifts)
|
||
|
||
|
||
def test_generate_is_idempotent(client, edition):
|
||
position = _create_position(client, edition)
|
||
resp = client.post(
|
||
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["created"] == 0
|
||
|
||
|
||
def test_extending_the_edition_then_generating_adds_the_new_days(client, edition):
|
||
position = _create_position(client, edition)
|
||
client.patch(f"/editions/{edition.id}", json={"end_date": "2026-07-14"})
|
||
|
||
resp = client.post(
|
||
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
|
||
)
|
||
# 2 jours de plus x 3 créneaux types.
|
||
assert resp.json()["created"] == 6
|
||
shifts = client.get(
|
||
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
|
||
).json()
|
||
assert len(shifts) == 18
|
||
|
||
|
||
def test_adding_a_day_part_then_generating_adds_it_everywhere(client, edition):
|
||
position = _create_position(client, edition)
|
||
client.post(
|
||
f"/editions/{edition.id}/day-parts",
|
||
json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00"},
|
||
)
|
||
resp = client.post(
|
||
f"{_positions_url(edition)}/{position['id']}/generate-shifts"
|
||
)
|
||
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(
|
||
f"/editions/{edition.id}/shifts", params={"position_id": position["id"]}
|
||
).json()[0]
|
||
client.patch(
|
||
f"/editions/{edition.id}/shifts/{shift['id']}",
|
||
json={"min_staff": 3, "ideal_staff": 5},
|
||
)
|
||
client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts")
|
||
again = client.get(f"/editions/{edition.id}/shifts/{shift['id']}").json()
|
||
assert again["min_staff"] == 3 and again["ideal_staff"] == 5
|