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>
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""
|
||
Test/démo du solveur : charge sample_request.json, résout, et imprime un
|
||
horaire lisible + le rapport de couverture (dispo vs requis). Sert aussi de
|
||
vérification (assert : statut faisable + chaque tech respecte ses contraintes).
|
||
|
||
python test_solver.py
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
from collections import defaultdict
|
||
from solver import solve_roster
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
|
||
def main():
|
||
with open(os.path.join(HERE, "sample_request.json")) as f:
|
||
req = json.load(f)
|
||
|
||
res = solve_roster(req)
|
||
|
||
print(f"\n=== STATUT : {res['status']} ({res.get('solve_ms')} ms) ===")
|
||
assert res["status"] in ("OPTIMAL", "FEASIBLE"), "solveur infaisable"
|
||
|
||
# Grille employé × jour
|
||
days = [req["horizon"]["start"]]
|
||
from datetime import date, timedelta
|
||
y, m, d = (int(x) for x in req["horizon"]["start"].split("-"))
|
||
start = date(y, m, d)
|
||
days = [(start + timedelta(days=i)).isoformat() for i in range(req["horizon"]["days"])]
|
||
|
||
by_tech = defaultdict(dict)
|
||
for a in res["assignments"]:
|
||
by_tech[a["tech_name"]][a["date"]] = f"{a['shift']}@{a['zone']}"
|
||
|
||
print("\n=== HORAIRE (Roster AI) ===")
|
||
hdr = "Technicien".ljust(16) + "".join(dd[5:].ljust(12) for dd in days)
|
||
print(hdr)
|
||
for t in req["technicians"]:
|
||
row = t["name"].ljust(16)
|
||
for dd in days:
|
||
row += (by_tech.get(t["name"], {}).get(dd, "·")).ljust(12)
|
||
print(row)
|
||
|
||
print("\n=== HEURES / TECH (équité, écart =", res.get("spread_hours"), "h) ===")
|
||
for tid, h in sorted(res["tech_hours"].items(), key=lambda kv: -kv[1]):
|
||
name = next((t["name"] for t in req["technicians"] if t["id"] == tid), tid)
|
||
print(f" {name.ljust(16)} {h} h")
|
||
|
||
print("\n=== COUVERTURE (dispo vs requis) ===")
|
||
for c in res["coverage_report"]:
|
||
flag = " ✅" if c["shortfall"] == 0 else f" ❌ MANQUE {c['shortfall']}"
|
||
print(f" {c['date']} {c['shift']:5} {c['zone']:10} requis={c['required']} assigné={c['assigned']}{flag}")
|
||
|
||
print("\n=== EXPLICATIONS ===")
|
||
for e in res["explanations"]:
|
||
print(" " + e)
|
||
|
||
# Vérifs dures
|
||
# 1) ≤ 1 shift/jour/tech
|
||
seen = set()
|
||
for a in res["assignments"]:
|
||
key = (a["tech"], a["date"])
|
||
assert key not in seen, f"double-booking {key}"
|
||
seen.add(key)
|
||
# 2) compétences respectées
|
||
skill_of = {t["id"]: set(t["skills"]) for t in req["technicians"]}
|
||
cov_skills = {(c["date"], c["shift"], c.get("zone", "—")): set(c.get("required_skills", []))
|
||
for c in req["coverage"]}
|
||
for a in res["assignments"]:
|
||
req_sk = cov_skills.get((a["date"], a["shift"], a["zone"]), set())
|
||
assert req_sk.issubset(skill_of[a["tech"]]), f"compétence manquante: {a}"
|
||
# 3) indispo respectée
|
||
unavail = {t["id"]: set(t.get("unavailable", [])) for t in req["technicians"]}
|
||
for a in res["assignments"]:
|
||
assert a["date"] not in unavail[a["tech"]], f"assigné un jour indispo: {a}"
|
||
|
||
print("\n✅ Toutes les contraintes dures respectées (1 shift/jour, compétences, indispos).")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|