Solveur OR-Tools (services/roster-solver) : couverture, compétences, équité, coût chargé, cadence/efficacité, capacité-par-job ; contraintes dures/souples façon Timefold. Hub (lib/roster.js) : génération via solveur, publication par réécriture de semaine (anti-doublons), demande (effectif ou nb de jobs), cadence/coût/ compétences par tech, pause, congés (Tech Availability + approbation), booking (slots roster-aware / fit 3-dispos / confirm) + portail public /book. Réessai sur serialization failures frappe_pg ; appels ERP séquentiels. Ops : page Planification (grille compacte « J8 », multi-shift, drag-select + undo/redo, modèles de semaine, éditeur cadence&coût, congés, SMS opt-in), page Rendez-vous (répartiteur), jobColor tech en pause → tickets rouges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.7 KiB
Markdown
69 lines
2.7 KiB
Markdown
# Roster AI — solveur d'horaires (OR-Tools CP-SAT)
|
|
|
|
Microservice d'**optimisation sous contraintes** (« façon Timefold », mais Python
|
|
sans JVM) qui génère des horaires de techniciens. Agnostique du backend : il ne
|
|
connaît ni Frappe ni ERPNext — on lui passe des techs + dispos + besoins de
|
|
couverture, il rend des assignations optimales.
|
|
|
|
## Place dans l'architecture
|
|
```
|
|
[Ops « Planification »] → [targo-hub lib/roster.js] → [roster-solver /solve]
|
|
│ lit techs/jobs (Dispatch Technician/Job, ERPNext)
|
|
└→ écrit les assignations validées
|
|
```
|
|
Le hub rassemble les entrées (techs, compétences, dispos, congés, besoins par
|
|
jour/zone), appelle `/solve`, puis écrit les assignations retenues.
|
|
|
|
## API
|
|
- `GET /health`
|
|
- `POST /solve` → corps :
|
|
|
|
```jsonc
|
|
{
|
|
"horizon": { "start": "2026-06-08", "days": 7 },
|
|
"shift_templates": [
|
|
{ "id": "jour", "name": "Jour 8h-16h", "hours": 8 }
|
|
],
|
|
"technicians": [
|
|
{ "id": "T001", "name": "Marc", "skills": ["fibre"],
|
|
"max_hours_week": 40, "max_days": 5, "cost_per_h": 38,
|
|
"zone_home": "Montréal", "preferred_off": ["sam","dim"],
|
|
"unavailable": ["2026-06-10"] } // dates de congé/pause
|
|
],
|
|
"coverage": [
|
|
{ "date": "2026-06-08", "shift": "jour", "zone": "Montréal",
|
|
"required": 2, "required_skills": ["fibre"] }
|
|
],
|
|
"weights": { "uncovered": 1000, "fairness": 5, "cost": 1, "preference": 8, "continuity": 4 },
|
|
"max_seconds": 10
|
|
}
|
|
```
|
|
|
|
Réponse : `assignments[]`, `coverage_report[]` (requis vs assigné vs **shortfall**),
|
|
`tech_hours{}`, `spread_hours`, `total_shortfall`, `explanations[]`, `status`.
|
|
|
|
## Modèle de contraintes
|
|
**Dures** (faisabilité) :
|
|
- 1 shift max par tech par jour
|
|
- compétences : un tech ne compte pour un poste que s'il a *toutes* les compétences requises
|
|
- disponibilité : pas d'assignation un jour de congé/pause (`unavailable`)
|
|
- `max_hours_week`, `max_days`
|
|
|
|
**Souples** (objectif pondéré, minimisé) :
|
|
- `uncovered` — postes non couverts (priorité écrasante → on voit les manques au lieu d'échouer)
|
|
- `fairness` — écart d'heures max-min entre techs
|
|
- `cost` — coût horaire total
|
|
- `preference` — pénalité de travail un jour « préféré off »
|
|
- `continuity` — *bonus* si la zone du poste = zone d'attache du tech (moins de déplacement)
|
|
|
|
> Couverture en **soft** = le solveur rend toujours le meilleur horaire possible
|
|
> même en sous-effectif, et **rapporte les manques** (ta demande « dispo vs requis »).
|
|
|
|
## Dév
|
|
```bash
|
|
python -m venv .venv && . .venv/bin/activate
|
|
pip install -r requirements.txt
|
|
python test_solver.py # démo + vérifs sur sample_request.json
|
|
uvicorn app:app --port 8090 # serveur
|
|
```
|