Disponibilités déclarées supprimées (table `availability`, migration 0022, API, éditeur de la fiche bénévole, badge de couverture du panneau d'affectation, colonnes des statistiques) ainsi que les règles OUTSIDE_AVAILABILITY, DAILY_HOURS_EXCEEDED et INSUFFICIENT_REST, les malus de disponibilité et de repos du solveur et le filtre « Surchargés » de la vue bénévoles. Seul le chevauchement reste contrôlé. La case « je suis disponible » du formulaire public est conservée. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.7 KiB
Python
75 lines
2.7 KiB
Python
"""Types d'entrée/sortie du solveur — suggestions, pas d'affectation automatique.
|
|
|
|
Le solveur ne connaît que ces structures simples (jamais les modèles
|
|
SQLModel), comme le moteur de validation dont il réutilise les briques
|
|
(`app.validation.models`).
|
|
"""
|
|
|
|
import datetime as dt
|
|
from dataclasses import dataclass, field
|
|
|
|
from app.validation.models import ShiftInfo
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SolverShift:
|
|
"""Un créneau à compléter (`assigned < ideal_staff`)."""
|
|
|
|
id: str
|
|
position_id: str
|
|
starts_at: dt.datetime
|
|
ends_at: dt.datetime
|
|
min_staff: int
|
|
ideal_staff: int
|
|
already_assigned: int
|
|
# Créneau(x) — n'importe quel poste — dont la fin coïncide avec le début
|
|
# de celui-ci, le même jour (vide s'il n'y en a pas) : sert au malus de
|
|
# chaînage (voir `engine._score`), pour limiter les enchaînements de
|
|
# shifts plutôt que cibler la rotation d'un poste en particulier.
|
|
predecessor_shift_ids: frozenset[str] = frozenset()
|
|
# Bénévoles déjà affectés à ce créneau (décisions déjà prises) : sert au
|
|
# bonus de compagnon de route (voir `engine._score`).
|
|
assigned_volunteer_ids: frozenset[str] = frozenset()
|
|
|
|
@property
|
|
def shift_info(self) -> ShiftInfo:
|
|
return ShiftInfo(id=self.id, starts_at=self.starts_at, ends_at=self.ends_at)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SolverVolunteer:
|
|
id: str
|
|
wished_position_ids: frozenset[str] = frozenset()
|
|
# Bénévoles (0 à N) avec qui il voudrait être affecté. Le vœu est saisi à
|
|
# sens unique (`VolunteerCompanionWish`) mais rendu réciproque ici par le
|
|
# loader : si A souhaite B, l'ensemble de B contient aussi A.
|
|
wished_companion_ids: frozenset[str] = frozenset()
|
|
# Passages des artistes que le bénévole veut voir (heure de passage),
|
|
# sous forme de fenêtres datées : un créneau qui chevauche l'une d'elles
|
|
# lui ferait manquer le passage.
|
|
wished_artist_windows: tuple[ShiftInfo, ...] = ()
|
|
# Créneaux déjà occupés (affectations existantes, décisions figées) :
|
|
# comptent pour chevauchement et chaînage.
|
|
existing_shifts: tuple[ShiftInfo, ...] = ()
|
|
# Total déjà affecté sur l'édition (minutes), pour répartir la charge.
|
|
existing_minutes: int = 0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SolverInput:
|
|
shifts: tuple[SolverShift, ...] = field(default_factory=tuple)
|
|
volunteers: tuple[SolverVolunteer, ...] = field(default_factory=tuple)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ShiftSuggestion:
|
|
"""Bénévoles suggérés pour un créneau, meilleur en tête."""
|
|
|
|
shift_id: str
|
|
volunteer_ids: tuple[str, ...]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SolverSolution:
|
|
suggestions: tuple[ShiftSuggestion, ...] = field(default_factory=tuple)
|