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>
108 lines
6.7 KiB
Markdown
108 lines
6.7 KiB
Markdown
# Module Shifts — spécification (intégré au Dispatch, sans paie)
|
||
|
||
> **Statut : spec v2 (2026-06-03) — architecture résolue.**
|
||
> Parcours : on a d'abord exploré **Frappe HR sur PostgreSQL** (déployé+validé à
|
||
> hr.gigafibre.ca, cf. `reference_frappe_hr_postgres.md`) — mais (1) son UI Desk
|
||
> n'est pas assez conviviale (l'usager la compare à Odoo Planning) et (2) elle est
|
||
> **déconnectée des vrais techs/jobs** qui vivent dans l'ERPNext de FACTURATION
|
||
> (`Dispatch Technician` / `Dispatch Job`). → On revient donc à la conclusion d'origine :
|
||
> **build-our-own, intégré au domaine dispatch.**
|
||
>
|
||
> **Décisions arrêtées :**
|
||
> - **Moteur (« Roster AI ») = OR-Tools CP-SAT** (Python), BÂTI + validé →
|
||
> `services/roster-solver/`. Contraintes dures/souples « façon Timefold ».
|
||
> - **Données du roster = dans l'ERPNext de facturation** (custom doctypes, à côté
|
||
> de Dispatch Technician/Job) → intégration triviale des features dispatch.
|
||
> - **UI = page « Planification » dans Ops** (Vue/Quasar, style Odoo : grille Gantt,
|
||
> drag-drop, bouton « Générer »), à côté du Dispatch.
|
||
> - L'instance Frappe HR (hr.gigafibre.ca) n'est PAS le foyer du roster (techs
|
||
> ailleurs). Gardée seulement si on veut de la vraie RH (soldes de congés) plus tard.
|
||
>
|
||
> **Features demandées par l'usager (2026-06-03) :** modèles de shifts → **dispo vs
|
||
> requis** par jour ; **mettre un tech en pause → ses tickets dispatch passent en
|
||
> ROUGE** ; demande de **vacances** ; demande de **modification de shift**.
|
||
|
||
## 1. Périmètre
|
||
|
||
Gestion de shifts + **approbations**. **Pas de paie.** On pousse les heures
|
||
approuvées vers ERPNext par API (la paie/compta reste côté ERPNext/legacy).
|
||
|
||
Validé contre la page Frappe HR « Shifts & Attendance » — toutes les fonctions
|
||
shift/approbation sont couvertes (voir mapping §7).
|
||
|
||
## 2. Contraintes transverses
|
||
- **Multitenant** : `org_id` sur chaque doctype (shared-DB), pour usage interne
|
||
Targo/Gigafibre **et** revente future à des tiers.
|
||
- **SSO** : Authentik OIDC (comme toutes les apps — cf. `reference_authentik`).
|
||
- **Stockage** : Postgres (hub ou base dédiée). Pas de dépendance ERPNext pour le
|
||
fonctionnement; sync sortante par API.
|
||
|
||
## 3. Modèle de données (doctypes / tables)
|
||
|
||
| Entité | Champs clés |
|
||
|---|---|
|
||
| **ShiftType** | `org_id`, nom, heure_début, heure_fin, durée_pause, seuil_demi_journée (h), seuil_absent (h), **grâce_retard** (min), **grâce_sortie_hâtive** (min), marge_pointage_avant/après (min), couleur |
|
||
| **ShiftAssignment** | `org_id`, employee_id, shift_type_id, date_début, date_fin, statut (Active/Inactive), source (manual/schedule/swap) |
|
||
| **ShiftSchedule** | `org_id`, nom, patron récurrent (RRULE hebdo), shift_type, équipe/employés, fenêtre de génération |
|
||
| **ShiftRequest** | `org_id`, employee_id, shift_type_id, date(s), motif, **approver_id**, statut (Draft→Submitted→Approved/Rejected), historique |
|
||
| **ShiftSwapRequest** | `org_id`, requester_id, counterparty_id, assignment_a, assignment_b, statut (Proposé→Accepté_par_pair→Approuvé_gestionnaire/Rejeté) |
|
||
| **Punch** (EmployeeCheckin) | `org_id`, employee_id, type (IN/OUT), timestamp, lat, lon, **job_id** (réf dispatch), device, source (mobile/web), within_geofence (bool) |
|
||
| **AttendanceDay** | `org_id`, employee_id, date, shift_assignment_id, statut (Présent/Retard/Demi/Absent), heures_calculées, première_IN, dernière_OUT |
|
||
| **AttendanceRequest** (régularisation) | `org_id`, employee_id, date, type, valeur_demandée, motif, **approver_id**, statut |
|
||
|
||
## 4. Workflows d'approbation
|
||
- **ShiftRequest** : employé soumet → `approver` (gestionnaire/département) approuve/rejette
|
||
→ si approuvé, crée/maj la `ShiftAssignment`. Multi-niveaux possible (chaîne d'approbateurs).
|
||
- **ShiftSwapRequest** : demandeur propose → **le pair accepte** → **gestionnaire approuve**
|
||
→ échange des deux `ShiftAssignment`. Tout refus annule.
|
||
- **AttendanceRequest** : employé demande correction → `approver` approuve → maj `AttendanceDay`.
|
||
- Approbateurs : configurables par employé / équipe / org. Notifs in-app + (option) SMS via le hub (Twilio).
|
||
|
||
## 5. Différenciateur — géofence vs **adresse du job dispatch**
|
||
À chaque punch terrain, on calcule la distance entre `(lat,lon)` du punch et
|
||
l'**adresse géocodée du job dispatch assigné** (RQA address DB), pas un lieu fixe.
|
||
`within_geofence = distance ≤ rayon_org` (configurable, ex. 150 m). Le punch hors
|
||
rayon est accepté mais **flaggé** (preuve de présence pour le dispatch).
|
||
|
||
## 6. Auto-attendance & auto-rostering
|
||
- **Auto-attendance** : à la clôture du shift, `AttendanceDay` est calculé depuis les
|
||
punchs vs le `ShiftType` (retard si IN > début+grâce ; sortie hâtive si OUT < fin−grâce ;
|
||
demi-journée/absent selon seuils d'heures).
|
||
- **Auto-rostering** : **Timefold Solver** (Apache-2.0, successeur d'OptaPlanner) —
|
||
même moteur que le dispatch par tags/compétences (`feedback_dispatch_tags`).
|
||
Contraintes : couverture des shifts, compétences requises, disponibilités,
|
||
équité, coûts. Génère des `ShiftAssignment` proposées (révisables).
|
||
|
||
## 7. Mapping Frappe HR → ce build (validé)
|
||
| Frappe HR | Ici |
|
||
|---|---|
|
||
| Shift Type (seuils, grâce) | ShiftType |
|
||
| Shift Assignment (+ outil lot) | ShiftAssignment (+ assignation en lot) |
|
||
| Repeating schedules / rotating | ShiftSchedule + Timefold |
|
||
| Shift Request + approbation multi-niveaux | ShiftRequest (workflow) |
|
||
| Roster drag-and-drop + échange | Vue roster + ShiftSwapRequest |
|
||
| Check-in/out mobile + géoloc | Punch (app field-tech) |
|
||
| Auto-attendance, retard/sortie hâtive | AttendanceDay |
|
||
| Attendance regularization | AttendanceRequest |
|
||
| Rapports/calendrier | Vues calendrier + exports |
|
||
| Paie | ❌ hors périmètre — push heures → ERPNext API |
|
||
|
||
## 8. Écrans
|
||
- **Ops (desktop)** : calendrier roster (drag-and-drop), définition ShiftType,
|
||
assignation en lot, file d'approbations (ShiftRequest/Swap/AttendanceRequest),
|
||
tableau de présence, déclencheur Timefold.
|
||
- **App field-tech** (déjà existante) : mon shift du jour, **punch IN/OUT** (géoloc
|
||
vs job), demander un shift / un échange, demander une régularisation.
|
||
|
||
## 9. Intégrations
|
||
- **Authentik OIDC** : auth + rôles (employé / gestionnaire / admin org).
|
||
- **Dispatch** : `Punch.job_id` ↔ jobs ; le solver partage les contraintes de skills/tags.
|
||
- **ERPNext** : push des heures approuvées (par employé/période) via l'API REST
|
||
(le hub a déjà `erpFetch` + token). Aucune dépendance hrms.
|
||
|
||
## 10. Phasage
|
||
- **MVP** : ShiftType, ShiftAssignment (+ lot), ShiftRequest + approbation, Punch
|
||
géoloc-vs-job, AttendanceDay auto, calendrier roster. Mono-org (Targo).
|
||
- **v2** : ShiftSchedule récurrent + **Timefold** auto-roster, ShiftSwap,
|
||
AttendanceRequest, multitenant `org_id` + revente, push ERPNext.
|