# Architecture — smartcentraldeveloppement

> Document généré par exploration automatisée du code le 2026-08-18, **rafraîchi le 2026-09-07** (intégration Massia, refonte du module `bl` en onglets, routes `devis_laravel`/`formule_modification_groupee`), **puis le 2026-09-09** (migrations de données pour `t_norme_contrainte` et `t_code_norme`, voir §4 et §8), **puis le 2026-09-29** (checksum sur le transfert des formules, voir §6sexies ; correctif des routes manquantes + ajout d'un mode « ajout produit/dosage » avec droit dédié sur la modification groupée de formule, voir §6septies), **puis le 2026-10-18** (état des connexions centrales et carte des chantiers/toupies du jour, nouvelles colonnes `centrale_latitude`/`centrale_longitude`, voir §6duodecies), **puis le 2026-10-20** (état des camions, voir §6duodecies). Objectif : servir de carte de référence pour naviguer le projet sans avoir à tout relire à chaque demande de modification. À mettre à jour périodiquement (le code évolue vite, ~160 migrations en ~1 an).
>
> **Portée de l'analyse** : exploration exhaustive de la structure des dossiers (tous les fichiers listés), lecture approfondie d'un échantillon représentatif (~65 fichiers : contrôleurs les plus gros et les plus petits, modèles, migrations, config, middlewares, providers, services, traits, vues, + le module Massia ajouté le 2026-09-07). Les généralisations sur les ~140 contrôleurs et ~150 modèles s'appuient sur cet échantillon et sur la convention de nommage très régulière observée — à affiner si un module précis s'avère être une exception.

---

## 1. Vue d'ensemble

**smartcentraldeveloppement** est un ERP métier pour **centrales à béton** (production, logistique, commande, facturation, qualité/traçabilité, conformité normative béton NF EN 206). C'est une application Laravel monolithique, développée et enrichie en continu depuis plusieurs années (traces d'un existant PHP procédural progressivement porté vers Laravel).

**Stack technique :**

| Couche | Techno |
|---|---|
| Framework | Laravel **10.10** (PHP ^8.1) |
| Auth web | Laravel **Jetstream 4.3** + **Fortify** (session, 2FA activé) |
| Auth API | Laravel **Sanctum 3.3** (tokens Bearer + "abilities") |
| Front-end module métier | Blade + **jQuery/Ajax** + **DataTables** (`yajra/laravel-datatables-oracle`) |
| Front-end socle Jetstream | Blade components + **Livewire 3** + Alpine.js |
| Build assets | **Vite** + **Tailwind 3** |
| Base de données | MySQL (via `doctrine/dbal`), driver session/queue `database` |
| Génération documents | `phpoffice/phpspreadsheet`, `phpoffice/phpword`, `maatwebsite/excel`, `setasign/fpdf`+`fpdi`, `spipu/html2pdf`, `mikehaertl/php-pdftk` |
| Reporting | `koolreport/core` + `koolreport/laravel` |
| Fichiers distants | `league/flysystem-ftp`, `league/flysystem-sftp-v3`, `phpseclib/phpseclib` |
| Qualité code (dev) | `larastan/larastan` (PHPStan), `laravel/pint`, `barryvdh/laravel-ide-helper`, `laravel-debugbar` |

---

## 2. Structure des dossiers

```
app/
├── Actions/        → Fortify/Jetstream (scaffolding standard, peu touché)
├── Console/
│   ├── Commands/   → tâches planifiées (export tarifs, récupération FTP...)
│   └── Kernel.php
├── Exceptions/
├── Exports/        → exports Excel (formules, pesées) via Maatwebsite/PhpSpreadsheet
├── fonctions/       → ⚠️ helpers procéduraux "legacy" (voir §7)
├── Http/
│   ├── Controllers/ → ~140 contrôleurs, 1 par module métier
│   └── Middleware/   → middlewares standards Laravel + 3 middlewares custom (voir §5)
├── Jobs/            → envoi de mails asynchrones (queue)
├── Listeners/        → hooks sur login/logout, échec d'envoi de mail
├── Mail/             → Mailables (commande, facture, mailing, 2FA...) ; trait `envoi_mail_fonction` (`ConfirmReadingTo()` = accusé de lecture façon PHPMailer)
├── Models/           → ~150 modèles, 1 par table `t_xxx`
├── Providers/
├── Reports/          → rapports KoolReport (ex. chiffre d'affaires par société)
├── Services/         → logique métier orientée objet (tarification, PEPPOL, mailing...)
├── Traits/            → ⚠️ traits utilisés comme "actions de contrôleur" (voir §7)
└── View/Components/  → composants Blade Jetstream (AppLayout, GuestLayout)

routes/
├── api.php    → routes API (Sanctum), petit fichier, peu de groupes
├── web.php    → ne contient plus de routes (commentaire pointeur)
├── web/       → routes web, 1 fichier par groupe de routes (147 fichiers à plat, nom = préfixe),
│               chargés automatiquement par RouteServiceProvider (glob) — voir docs/routes.md
├── console.php
└── channels.php

resources/
├── views/     → ~140 sous-dossiers (1 par module), pattern index.blade.php + form.blade.php
│   ├── fiche_annexe/    → mini-tableaux réutilisables en onglets (le seul vrai composant partagé)
│   ├── doc_utilisateur/ → documentation utilisateur intégrée, par module (bonne pratique rare)
│   └── components/       → composants Jetstream par défaut
├── js/ et css/  → très légers, l'essentiel du JS métier est inline dans les vues Blade
└── lang/         → fr/en

database/
├── migrations/  → ~160 fichiers, convention de nommage mixte (voir §6)
└── seeders/      → DatabaseSeeder par défaut uniquement

tests/
├── Feature/  → tests Jetstream par défaut + quelques modules métier (Ajout, Chauffeur,
│               Facture, Produit, Service, Zone, SocieteMailing)
│             + ApiTransfert/ : 1 test par fiche de /transfert/{fiche} (voir §6)
└── Unit/     → quasiment vide (ExampleTest uniquement)
```

---

## 3. Cartographie des modules métier

Les contrôleurs/modèles/vues suivent presque tous la convention `<domaine>_controller.php` / `<domaine>.php` (modèle) / `views/<domaine>/`. Voici le regroupement par grand domaine fonctionnel (utile pour savoir "où chercher") :

**Commercial / tiers**
client, contact, commercial, categorie_client, famille_client, sous_famille_client, typologie_client, region_commerciale, secteur, concurrent, adresse_facturation, banque, mode_reg, echeance, assurance_credit, categorie_assurance_credit, position_compte, commentaire_stocker, devis.

**Chantiers**
chantier, planning, planning_commun, planning_prev, ouvrage, type_ouvrage, ventilation, zone.

**Formulation béton (production)**
formule, formule_variant, formule_producteur, composition, classe_consistance, classe_exposition, classe_resistance, classe_chlorure, classe_reduction_prg, designation_beton, type_beton, code_norme, norme, norme_contrainte, specificite, nature_addition, reglage_trappe, groupe_centrale_formule, famille_beton_echantillon, gwr, ajout, article_associe, formule_modification_groupee (remplacement/ajout groupé de produit sur plusieurs formules, voir §6septies), formule_copie (Laravel, copie détail + composition d'une formule d'une centrale vers une autre, voir §6nonies).

> `norme_contrainte` (table `t_norme_contrainte`) porte les valeurs limites par classe d'exposition (rapport Eeff/liant, classe de résistance mini, et surtout le rapport maximal Addition/(ciment+Addition) par type de ciment/addition) issues du tableau **NA.F.1 du PR NF EN 206** — consommée en lecture par `composition_calcul_trait::calculer_ratio_max_addition()` (nom indicatif, voir le trait). `code_norme` (table `t_code_norme`) est en réalité une **simple table de vocabulaire contrôlé** sans clé étrangère (colonnes `classe_resistance`/`classe_exposition`/`type_ciment`/`type_addition` lues indépendamment via `->distinct()` pour peupler des listes déroulantes "Code norme") — elle n'est pas liée à `norme` (`t_norme`, actuellement **0 ligne**) ni à `norme_contrainte` malgré la proximité de nom.

**Centrales & parc matériel**
centrale, chauffeur, vehicule, type_vehicule, gamme_vidange, moyen_dechargement, produit, produit_centrale, produit_transport, fournisseur, typologie_article, famille_article, entree_matiere, energie, trajet, transporteur. La centrale a aussi une page d'état des connexions (`/centrale/etat_connexion`, voir §6duodecies).

**Commandes / logistique / livraison**
commande, ligne_commande, bl, ligne_bl, pesee, depart_cycle, suivi_numero_bl, suivi_livraison (dont la carte des chantiers et toupies du jour et l'état des camions, voir §6duodecies), reservation_pompe, info_commande, transfert_vers_production, suivi_commande, api_transfert (API). Voir aussi devis (module `devis_laravel`, routes dédiées) qui se transforme en commande/facture/BL.

> ⚠️ **`resources/views/bl/*` est actuellement non commité (untracked)** alors que `bl_controller.php` (tracké) en dépend directement (`view('bl.index')`, `view('bl.form')`...). Le module `bl` a été **récemment décomposé en onglets** (`bl_onglet_general`, `bl_onglet_divers`, `bl_onglet_heures`, `bl_onglet_incident`, `bl_onglet_demat`, `bl_onglet_adresse_facturation/livraison`, `bl_onglet_observation`) — première application concrète de la recommandation §9 étape 3.12 (factoriser les formulaires monolithiques à la manière de `fiche_annexe/`). À committer avant toute synchronisation/déploiement pour ne pas perdre ce travail.

**Facturation**
facture, ligne_facture, pre_facture, ligne_pre_facture, relever_facture (écrans legacy, PDF / mail / suppression en Laravel, voir §6quindecies), operation_facture, reglement, tva, groupe_tarif, groupe_centrale_tarif, groupe_societe_tarif, tarif, decalage_facturation, motif_avoir, export_facture_electronique (PEPPOL), export_comptabilite.

**Qualité / traçabilité**
tracabilite, tracabilite_composition, tracabilite_envoi, incident, gravite_incident, famille_incident, sous_famille_incident, nature_incident, type_attaque, type_exposition, type_finition.

**Administration / paramétrage**
parametre, profil, utilisateur, societe, civilite, pays, commune, domaine, unite, motif_action, motif_absence, motif_echec_reussite, type_action, type_demande_action, demande_action, message_demande_action, action, activite, fonction.

**Communication**
envoi_mail, envoi_sms, message_alarme_mails, mailing, mailing_template, mailing_history.

**Intégrations externes / imports / IA**
importation_externe, migration_client_centrale, migration_utilisateur, trans_externe, gpt (appel IA externe), etat_stock, cbao.

**Outils internes / dev**
test_copier_fichier_ftp, test_excel, test_impression, test_trans_externe, statistique, cocher_fiche_centrale, modele_impression, modele_qr_code.

---

## 4. Conventions de code observées

- **Contrôleurs** : `<domaine>_controller.php`, `extends BaseController`. Méthodes standard récurrentes : `index()` (vue liste), `data_table()` (endpoint JSON pour DataTables), `afficher_formulaire()` (retourne le HTML du form, chargé en Ajax), `enregistrer()` (create/update), `supprimer()`, `<domaine>_recherche()`. `BaseController` charge `$this->user` et injecte plusieurs helpers de `App\fonctions` + traits métier, mais contient peu de logique propre.
- **Modèles** : `<domaine>.php`, `extends BaseModel` (sauf `utilisateur`/`User` qui restent proches d'Eloquent standard pour Jetstream/Sanctum). Convention systématique `$table = 't_xxx'`, `$primaryKey = 'id_xxx'` (PK non standard), `$guarded = ['created_at','updated_at']` (pas de `$fillable`). Relations **toutes préfixées `lier_`** (`lier_client()`, `lier_centrale()`...) — convention maison, pas du Eloquent idiomatique. `BaseModel` ajoute un hook `saving` qui interroge `SHOW COLUMNS` pour poser des valeurs par défaut par type — mécanisme non standard.
- **Tables** : préfixe `t_` systématique (`t_client`, `t_commande`, `t_formule`...) sauf les tables du socle framework (`users`, `personal_access_tokens`).
- **Vues** : `views/<domaine>/index.blade.php` (grille DataTables serveur) + `form.blade.php` (formulaire chargé en Ajax et injecté dans une `div`, sans rechargement de page). `fiche_annexe/table_xxx_fiche_annexe.blade.php` = mini-composant générique réutilisé en onglet sur plusieurs fiches (client, chantier...) — seul vrai mécanisme de réutilisation de vue identifié.
- **Migrations** : deux formats coexistent — le format standard Laravel (`YYYY_MM_DD_HHMMSS_nom.php`) pour l'historique ancien/framework, et un format maison récent `YYYY_MM_DD_HHmm_V<version>_<INITIALES>_<description>.php` (ex. `2025_08_12_0920_V900_NB_create_t_reglage_trappe.php`) qui trace la version de release et l'auteur directement dans le nom de fichier.
- **Migrations de données** (pas seulement de schéma) : pattern récurrent observé sur plusieurs migrations (`2026_03_23_1330_V904000_BAM_t_client_assurance.php`, `2026_07_29_1410_V905000_GC_t_formule_variant_active.php`, et les deux ajoutées le 2026-09 pour `t_norme_contrainte`/`t_code_norme`) : `Schema::hasTable()`/`Schema::hasColumn()` en garde avant un `Schema::create`/`table`, puis `DB::table(...)->updateOrInsert(...)` ou `->insert()` conditionné (table vide / valeur absente) pour rendre la migration rejouable sans doublon sur un environnement où les données existent déjà. Il n'existe **aucun seeder** dans `database/seeders/` (`DatabaseSeeder` par défaut, vide) — toutes les données de référence "métier" sont poussées via ce type de migration plutôt que via des seeders Laravel classiques.
- **Fenêtres de recherche legacy** (`#affiche_<module>`, ex. `#affiche_generation_facture`) : un même conteneur, défini dans `public/inc/css/style.css` et partagé par beaucoup de pages, reçoit successivement toutes les recherches d'un écran (société, client, chantier…). Pour élargir **une seule** de ces listes, mettre un `<style>` dans le fichier PHP de cette liste avec `#affiche_xxx:has(#id_table) { … }` plutôt que de modifier `style.css`. Le bouton fermer (`.fermer`) est posé sur le voile `#filtre` (z-index 10), à 5 % du bord droit, sous la fenêtre (z-index 11) : au-delà d'environ 75 % de largeur centrée, la fenêtre le recouvre. Exemple : liste des chantiers de la génération de facture/préfacture, à 70 % avec adresse, code postal et ville (2026-10-22, [docs/generation_facture_liste_chantiers_adresse.md](docs/generation_facture_liste_chantiers_adresse.md)).
- **Cases à cocher booléennes** (droits `t_profil.profil_*` : `profil_devis_comptant_uniquement`, `profil_limiter_centrale`, `profil_visualiser_cout_*`...) : la vue envoie un `<input type="hidden" value="0">` avant la checkbox (formulaire sérialisé en Ajax), donc le champ est **toujours présent** dans la requête. Côté contrôleur, lire la valeur avec `$request->boolean('champ')`, jamais `$request->has('champ')` (toujours vrai avec le champ caché). Bug corrigé dans `profil_controller::enregistrer()` (le `has()` passait les 8 cases à 1 à chaque enregistrement depuis le 12/09/2026, ce qui grisait par exemple Valider sur les devis legacy) — voir `docs/profil_cases_a_cocher_toujours_cochees.md`.

---

## 5. Authentification & sécurité

- **Web** : Jetstream + Fortify, guard `session`, 2FA activable par utilisateur (`TwoFactorAuthenticatable`), rate limiters natifs sur `login` (5/min) et `two-factor` (5/min). Authentification custom (`FortifyServiceProvider`) : recherche par `email`, `name` OU `code_auto` ; migration legacy des mots de passe SHA1 → bcrypt à la première connexion réussie (`user_ancien_mdp`).
- **API** : Laravel Sanctum, tokens personnels (`HasApiTokens` sur `User`), système d'**abilities** natif Sanctum exploité via le middleware custom `CheckTokenAbility` (alias `ability`, ex. `ability:read`).
- **Middlewares custom** :
  - `CheckUserInterne` (alias `interne`) : bloque l'accès si `user_type != 0` (distingue utilisateurs internes/externes), redirige vers `/bl` sinon.
  - `VerifierSessionUtilisateur` (groupe `web`) : recopie les attributs de l'utilisateur connecté dans `$_SESSION` PHP natif — pont vers du code legacy hors cycle de vie Laravel.
  - `CheckTokenAbility` (alias `ability`) : vérifie `tokenCan()` côté Sanctum.
- **`routes/api.php` (état actuel, mis à jour ce jour)** : `/user` et le groupe `v1/classe_consistance` sont sous `auth:sanctum` ; le groupe `/transfert` vient d'être protégé par `auth:sanctum` également (voir historique de conversation) ; `/centrale_connectee` reste **sans middleware d'authentification** — à examiner si ce point d'entrée doit rester public. `POST /connexion_centrale` (ajouté 2026-09-16, sous `auth:sanctum`) : check-in envoyé par l'automate/gestion d'une centrale avec ses versions logicielles (`centrale_controller::connexion_centrale()`) — réutilise les colonnes `centrale_version_bhp_gestion`/`centrale_version_bhp_process`/`centrale_version_prog_automate` déjà existantes sur `t_centrale` (pas de nouvelle migration) et met à jour `centrale_heure_derniere_communication` à `now()`. Payload en liste d'objets `{centrale_code, version_gestion, version_process, version_plc, user_bhp}`, une réponse par centrale dans le même ordre — même convention que `/transfert/{fiche}`.
- **Points d'attention sécurité relevés pendant le scan (à traiter, voir §8) :**
  1. **Clé API OpenAI en clair dans `gpt_controller.php`** — à révoquer/rotater immédiatement et déplacer en variable d'environnement.
  2. **Mots de passe FTP/SFTP/Trakkeo en valeur littérale par défaut** dans `config/filesystems.php` (`env('FTP_PASSWORD', 'valeur réelle')`) — mêmes recommandations.
  3. CORS totalement ouvert sur `api/*` (`allowed_origins/methods/headers => ['*']`).
  4. CSRF désactivé sur plusieurs routes à effet de bord (téléchargement PDF, envoi de mail, validation de transfert production).
  5. Dans `routes/web/*.php` (un fichier par groupe depuis le 2026-10-06), plusieurs groupes (chantier, commande, facture) n'appliquent le middleware d'auth qu'à la route `index`, laissant les routes d'écriture/suppression reposer sur des vérifications ad hoc dans le code des contrôleurs plutôt que sur le routeur — exactement le même type de trou que celui corrigé aujourd'hui sur `/transfert` dans `api.php`.
  6. `$guarded` limité à 2 colonnes sur des modèles sensibles (`facture`, `client`, `commande`) = mass assignment quasiment ouvert.
  7. `SESSION_SECURE_COOKIE` non forcé à `true`.

---

## 6. Intégrations externes

| Intégration | Où | Nature |
|---|---|---|
| Automate de centrale (temps réel) | `app/Traits/envoyer_socket.php` | Socket TCP brut (`socket_create`/`socket_connect`), requête/réponse synchrone vers `IP_AUTOMATE:PORT_AUTOMATE` — pas de WebSocket web, protocole propriétaire point-à-point |
| Automate de centrale (fichiers) | `app/Traits/copier_fichier_sftp.php` + `Console/Commands/tache_recuperer_fichier_xml_ftp.php` | Récupération SFTP de paires `.dat`/`.flg`, suppression distante après copie. ⚠️ La commande planifiée censée orchestrer ce flux est **vide** (juste un `Log::info`) — flux probablement non opérationnel actuellement, à vérifier |
| Facturation électronique | `app/Services/generer_peppol_bis3_xml_service.php` + `generer_data_peppol_par_facture_service.php` | Génération XML conforme **PEPPOL BIS3 / UBL 2.1**, mapping tolérant aux champs manquants. Pas de validation XSD/Schematron visible dans ce service |
| IA générative | `app/Http/Controllers/gpt_controller.php` | Appel à l'API OpenAI — ⚠️ clé en dur, voir §5 |
| Mail | Mailgun / Postmark / SES / SMTP selon environnement | Config dans `config/mail.php` + `services.php` |
| Fichiers plats clients externes | `app/Services/structure_client_externe_service.php` | Parsing de fichiers à largeur fixe |
| Trakkeo (télématique véhicules ?) | disque `ftp_trakkeo` dans `config/filesystems.php` ; envoi des BL XML par `app/Services/bon_livraison_xml_gps_service.php` et récupération (`OUT/<societe_code_demat>`) par `app/Services/integration_fichier_xml_bl_gps_service.php`, tous deux via `app/Services/trakkeo_disque_service.php::disque($root)` (FTP ou SFTP selon config `services.trakkeo`, connexion SFTP dans `trakkeo_sftp_connexion.php`) | FTP dédié ; **SFTP si `TRAKKEO_SFTP=true`** dans `.env`, vers un serveur distinct configuré par `TRAKKEO_SFTP_HOST/USERNAME/PASSWORD/PORT/ROOT_IN` (app.trakkeo.com:2022), adaptateur Flysystem SFTP avec vérification de l'empreinte hôte `TRAKKEO_SFTP_HOST_FINGERPRINT` au format OpenSSH `SHA256:…` ; en SFTP le webhook de notification est `TRAKKEO_WEBHOOK_SFTP_URL` avec le token `TRAKKEO_WEBHOOK_SFTP_TOKEN` dans l'en-tête `X-Trakkeo-Token` (sinon, token en query string `?token=` : `TRAKKEO_WEBHOOK_URL` / `TRAKKEO_WEBHOOK_TOKEN`), ajouté le 2026-10-10 |
| Broadcasting | Pusher configuré (`config/broadcasting.php`) | Présence de config, usage réel non confirmé dans l'échantillon |
| Reporting | KoolReport (`koolreport/core`, `koolreport/laravel`, `koolreport/amazing`, `koolreport/datagrid`, `koolreport/printpdf`, `koolreport/excel`) | Voir §6bis — module `statistique_laravel`, portage complet de l'ancien système `public/statistique/*` |
| MASSIA (logiciel qualité béton, éditeur Arcade) | `app/Services/Massia/`, `app/Http/Controllers/Massia/` | Voir §6ter — échange bidirectionnel BL (export) / Formule (import), **ajouté le 2026-09-07, pas encore branché** (aucune route, config ou migration) |
| Impression PDF (SumatraPDF) | `app/Services/impression_pdf_service.php`, `app/Http/Controllers/impression_pdf_controller.php` | Voir §6quater — impression du BL dématérialisé à partir d'un `bl_no_exec`, exposée en API, **ajouté le 2026-09-11** |
| Programme centrale (transferts de données) | `public/trans/action_trans_*.php` (scripts PHP procéduraux hors Laravel, via `public/inc/init.inc.php`) | Échange par requêtes GET, paramètres encodés en JSON (`_deserialize()`), `action=integration` (centrale → web) / `action=extraction` (web → centrale), file d'envoi `t_tracabilite_envoi`. Les formules sont contrôlées par checksum depuis le 2026-09-29, voir §6sexies |

---

## 6bis. Module statistiques (`statistique_laravel`)

Portage Laravel complet de l'ancien système de rapports PHP procédural `public/statistique/*` (piloté historiquement par `public/statistique/index.php` + un rapport par sous-dossier). **44 statistiques migrées** (toutes celles du menu `index.php`, plus les dossiers orphelins présents dans `public/statistique/` mais non reliés au menu). L'ancien système reste en place tel quel dans `public/statistique/` (non supprimé).

**Architecture** :
- `app/Http/Controllers/statistique_laravel_controller.php` : contrôleur unique avec un **registre** `slug => classe de rapport` (méthode `registry()`) et des routes génériques :
  - `GET /statistique_laravel/` → page maître de filtres (`index()`, vue `resources/views/statistique/index.blade.php`, reprend le formulaire de l'ancien `index.php` : dates, société/centrale/client/chantier/formule/..., affichage dynamique des critères pertinents par stat via `criteriaMap` en JS).
  - `GET /statistique_laravel/chantiers_par_client` : endpoint JSON pour la liste déroulante chantier dépendante du client choisi.
  - `GET /statistique_laravel/{slug}` → `show()` : rend la statistique en HTML (widget `\koolreport\datagrid\DataTables`).
  - `GET /statistique_laravel/{slug}/pdf` → `pdf()` : export PDF.
  - `GET /statistique_laravel/{slug}/excel` → `excel()` : export Excel.
  - ⚠️ Les recherches "avancées" en popup du formulaire legacy (`public/statistique/recherche_generale/*`) n'ont **pas** été portées ; remplacées par de simples listes déroulantes.
- Chaque statistique vit dans `app/Reports/<slug>/<slug>.php` (classe `extends App\Reports\Support\BaseReport`) + `<slug>.view.php` (web) + `<slug>.pdf.view.php` + `<slug>.excel.view.php`, sur le modèle de `chiffre_affaire_par_societe`/`bl_service_par_societe_centrale` (rapports d'origine, conservés tels quels, non ré-enregistrés dans le nouveau registre pour éviter les collisions de slug).

**Classes partagées** (`app/Reports/Support/`) :
- `BaseReport` : mutualise les traits communs (source Laravel/Eloquent, thème `amazing`, export PDF, export Excel, filtres).
- `FilterableReport` : construit les clauses `WHERE` + tableau de bindings PDO nommés (`buildFilters()`, `dateRangeFilter()`, `societeOuCentraleFilter()`) — **tous les filtres utilisent des requêtes préparées**, contrairement au legacy qui interpolait les valeurs `$_GET` directement dans le SQL (injection SQL possible, corrigée dans ce portage).
- `PdfGroupedTable` : rendu HTML générique d'un tableau groupé à N niveaux pour mpdf (qui n'exécute pas le JS du widget web), avec sous-totaux par niveau.
- `ExcelGroupedTable` : construit la config `rowGroup` du widget `\koolreport\excel\Table` (regroupement + sommes natif côté Excel, pas de recalcul PHP).
- `FastPdfExportable` / `FastPdfHandler` : variante de `koolreport/printpdf` sans `autoScriptToLang`/`autoLangToFont` (gain mesuré modeste, ~9% sur un run moyenné — l'essentiel du temps de génération PDF vient de la mise en page mpdf elle-même sur des tableaux de plusieurs milliers de lignes).
- `ZoneFactureQuantite` / `LigneBlAggregation` : logique métier partagée (quantité facturée d'une ligne "zone" encodée dans `ligne_facture_liste_bl`, calcul du total TTC ligne à ligne d'un BL avec TVA par type d'article) — reprise à l'identique du legacy sur les rapports où elle apparaissait dupliquée.
- `PalmaresReport` : classement décroissant générique pour les rapports "Palmarès" (client/formule/formule M3), basé sur des tables `t_palmares_*` pré-agrégées par un autre processus (pas de filtre de date).

**Corrections apportées par rapport au legacy** (au fil de la migration, cf. commentaires dans chaque classe concernée) :
- Filtres passés en binding PDO au lieu d'être concaténés dans le SQL (faille d'injection).
- Plusieurs rapports lisaient un filtre "chantier" (ou "société") sans jamais l'appliquer à la requête (variable calculée puis oubliée) — filtre effectivement branché.
- `facture_chiffre_affaire_chantier` groupait en réalité par client (copie de `facture_chiffre_affaire_client` jamais adaptée) — corrigé pour grouper par chantier.
- `facture_chiffre_affaire_formule` n'appliquait pas le flip de signe sur avoir (incohérent avec les rapports similaires) — corrigé.
- Bug de famille d'article : plusieurs rapports vérifiaient `famille_service_id` quel que soit le type de ligne (service/ajout/adjuvant/zone/formule n'ont pourtant pas tous cette colonne) — corrigé en résolvant la bonne colonne de famille selon le type réel.
- Date de fin avec un `$` littéral en trop dans une comparaison SQL (`bl_societe_centrale_formule_normee`) — corrigé.
- `journal_comptable`/`journal_vente` lisaient `date_debut`/`date_fin` sans jamais filtrer dessus — filtre de date ajouté sur `facture_date`.

**Limites connues** : le mécanisme d'export PDF du rapport `bl_service` legacy (`koolreport/export` + Chrome/Node headless) n'a pas été repris — ni Chrome ni Node ne sont installés sur cette machine de dev. Tous les nouveaux exports PDF utilisent `koolreport/printpdf` (mpdf, 100% PHP), avec un rendu HTML recalculé côté PHP (le widget web `DataTables` étant du JS, non exécutable par mpdf). Les packages premium KoolReport (`amazing`, `datagrid`, `printpdf`, `excel`) sont installés depuis le dépôt Composer privé `https://repo.koolreport.com` (identifiants dans `auth.json`, gitignored, non commité).

---

## 6ter. Intégration MASSIA (`app/Services/Massia/`, `app/Http/Controllers/Massia/`) — en cours, non branchée

Échange bidirectionnel avec **MASSIA**, le logiciel qualité/formulation béton de l'éditeur Arcade utilisé en centrale, ajouté le 2026-09-07 et **absent de tout autre point d'entrée du code à ce jour** (pas de route déclarée dans `web.php`/`api.php`, pas de clé `services.massia` dans `config/services.php`, pas de colonne `bl_exporte` en migration bien qu'utilisée par le code). Traiter ce module comme un **chantier ouvert**, pas encore fonctionnel en l'état.

**Sens sortant — export d'un BL vers Massia** :
- `App\Services\Massia\MassiaApiClient` : client HTTP bas niveau (`Illuminate\Support\Facades\Http`), authentification par en-tête `X-API-KEY` lu dans `config('services.massia.api_key')` (config à créer), timeout 30 s.
- `App\Services\Massia\BlMassiaMapper::mapper(bl $bl)` : construit le payload JSON `BatchProtocolDto` attendu par Massia à partir d'un `bl` (chantier, client, formule, gâchées/pesées groupées par `pesee_no_gachee`). Documente explicitement dans ses docblocks les champs du schéma Massia **sans équivalent actuel** (envoyés à `null` : `Certification`, `Hygrometry`, etc.) — bonne pratique de traçabilité des limites connues.
- `App\Services\Massia\BlMassiaExportService::exporter(bl $bl)` : appelle le mapper puis le client, journalise succès/échec (`Log::warning`/`Log::error` + `report()`), marque `$bl->bl_exporte = 1` en cas de succès. Retourne un tableau `['success' => bool, 'message' => ..., 'payload' => ..., 'reponse' => ...]` — jamais d'exception non interceptée, comportement adapté à un déclenchement batch.
- Deux points d'entrée pour déclencher l'export : `App\Console\Commands\massia_exporter_bl_command` (`php artisan massia:exporter-bl {bl_code?}` — sans argument, exporte tous les BL avec `bl_annule = 0` et `bl_exporte = 0` ; **non planifiée dans `Console/Kernel.php`**, à lancer manuellement pour l'instant) et `App\Http\Controllers\Massia\massia_bl_export_controller::exporter()` (pensé pour `POST /v1/massia/bl/{bl}/exporter`, route à créer).

**Sens entrant — import d'une formule depuis Massia** :
- `App\Http\Controllers\Massia\massia_formule_import_controller::importer()` (pensé pour `POST /v1/massia/formule`) valide `CodeBeton`/`Centrale.Code` puis délègue à `App\Services\Massia\FormuleMassiaImportService::importer(array $payload)`.
- Le service fait un `firstOrNew`/upsert sur `formule` (+ `formule_detail` par centrale) et **remplace intégralement** la composition (`composition::where(...)->delete()` puis recréation) pour la centrale concernée, dans une transaction DB unique.
- Depuis le 2026-09-17, `formule_detail` et `composition` sont en plus scopés par `formule_variant_id` (variante active résolue via `formule_variant::getActive()`, fallback `0`), pour rester cohérent avec le reste de l'appli (`formule_controller`, `composition_calcul_trait`) qui filtre toujours ces deux tables par la variante active — sans quoi un import Massia écrasait/laissait invisible la composition selon la variante en base. Voir [docs/massia_integration.md](docs/massia_integration.md).
- Les règles de correspondance de codes (préfixe centrale sur le code béton, transformation `X<code>` → `<code>` pour l'exposition, concaténation classe+résistance, `Cl0<code>` pour le chlorure, suffixe `--` sur les codes fournisseurs...) sont **documentées en docblock avec leur origine** ("Interface Massia-Alfi_Formules/Produits" fournie par Arcade) et leurs limites (règle chlorure déduite d'un seul exemple) — traçabilité métier soignée, à conserver comme modèle pour documenter les futures intégrations tierces.

**À faire avant mise en service** (voir aussi §9) : déclarer les routes `v1/massia/...` (avec middleware d'auth adapté — probablement une clé API dédiée côté entrant, symétrique à celle utilisée en sortant), ajouter `config/services.php['massia']` + variables `.env`, migrer la colonne `bl_exporte` sur `t_bl`, décider si `massia:exporter-bl` doit être planifiée (`Console/Kernel.php`) ou rester déclenchée à la demande.
| Reporting | KoolReport | Rapport "chiffre d'affaire par société" (`app/Reports/`) |
| Intégration BHP (fiches référentiel) | `app/Http/Controllers/api_transfert_controller.php` (`/transfert/{fiche}`, `auth:sanctum`) | Endpoint générique GET/POST/DELETE couvrant plusieurs fiches (`$modeles`/`$controleurs` : client, chantier, chauffeur, formule, vehicule, origine, fournisseur). |

`api_transfert_controller::enregistrer_fiche()` normalise le corps (objet unique ou liste) et, pour chaque fiche, appelle `resoudre_liaisons()` **avant** de déléguer au contrôleur métier : elle résout génériquement toute relation `belongsTo` `lier_*` transmise en objet imbriqué (ex. `"fournisseur": {"fournisseur_code": "..."}` → `fournisseur_id`), y compris le cas `null` explicite (→ FK à `0`, convention métier existante). Chaque contrôleur de fiche n'a donc plus qu'à exposer `enregistrer_api()` (résout son propre PK via son `*_code`, merge `<fiche>_id`, délègue à `enregistrer()`) et `supprimer_api()` (cherche par `code`, délègue à `supprimer($id, $api=true, $tracabilite_envoi_bhp, $user)`), suivant le format de réponse d'`origine_controller` (`success`/`message`/`errors`/`id_tracabilite_envoi_bhp`, `success` forcé à `true` en cas de blocage métier quand `$api` est vrai, pour éviter les boucles de retry côté BHP). ⚠️ Piège identifié : ne pas copier tel quel le fallback `''` d'`origine_controller`/`fournisseur_controller` pour l'auteur de traçabilité quand il n'y a pas de token Sanctum (formulaire web) — pour une fiche activement éditée en UI, garder `$this->user->name` en repli sous peine de blanchir la traça web. État au 2026-09-15 : origine et fournisseur historiquement câblés ; chauffeur, classe_chlorure, classe_consistance, classe_exposition, transporteur, vehicule ajoutés ; client/chantier/formule et les autres fiches référentiel restantes sont câblées fiche par fiche à la demande (voir `docs/api_transfert_bhp.md` pour le détail à jour). Une fiche peut être **écriture seule** : la liste `$fiches_sans_lecture` de `api_transfert_controller` fait refuser son GET (404 « Fiche inconnue »), POST/DELETE restent actifs (ajouté 2026-10-13 pour `gamme_vidange`, qui n'écrit aussi aucune `tracabilite_envoi`).

`enregistrer_fiche()` résout aussi, pour les fiches listées dans `$modeles_centrale` (chauffeur, client, vehicule pour l'instant — celles qui ont une relation `lier_<fiche>_centrale`), un tableau `centrales` consommé par le `sync()` du contrôleur métier : `t_parametre.parametre_integration_<fiche>` (colonne par fiche) à `1` → toutes les centrales actives ; à `0`/absent → uniquement la centrale émettrice (`centrale_code` du payload), en conservant les centrales déjà liées pour ne pas les effacer lors d'une mise à jour. Reprend le comportement de l'ancien endpoint legacy `public/trans/action_trans_chauffeur.php`. ⚠️ Les clés de tableau PHP sont sensibles à la casse (contrairement aux colonnes MySQL) : `vehicule` a une colonne `Vehicule_Code` et le payload BHP l'envoie avec cette casse exacte — `api_transfert_controller::$colonnes_code_speciales` mappe ces exceptions fiche par fiche. Détail complet (payloads, conventions par contrôleur, état d'avancement par fiche) : `docs/api_transfert_bhp.md`.

**Pivot `t_chantier_centrale` par ids (2026-10-05)** : dernier pivot centrale encore lié par codes (`chantier_code` = `t_chantier.chantier_code_complet`, `centrale_code`), il est maintenant lié par `chantier_id` (→ `t_chantier.id_chantier`) / `centrale_id` (→ `t_centrale.id_centrale`), comme `t_client_centrale`, `t_chauffeur_centrale`, `t_vehicule_centrale`... : `chantier::lier_chantier_centrale()` = `belongsToMany(centrale::class, 't_chantier_centrale', 'chantier_id', 'centrale_id')`, et côté web `chantier_controller`/`presence_centrale.blade.php` travaillent sur les `id_centrale` comme pour les autres fiches. `presence_centrale.blade.php` accepte un paramètre optionnel `prefixe_centrale` (préfixe des `id`/`for` des cases et du bouton) : la fiche chantier l'inclut avec `'chantier_'` car elle s'ouvre dans la fiche client, qui a ses propres cases centrales (2026-10-06). Migration `database/migrations/2026_10_05_1540_V905000_BAM_t_chantier_centrale_avec_id.php` (même principe que `t_client_centrale` en V900) : `chantier_code`/`centrale_code` conservés en colonnes legacy `NULL`able, plus remplies par Laravel. Toute lecture, suppression ou vérification d'existence sur ce pivot passe donc par les ids (jointure sur `t_chantier`/`t_centrale` quand un code est nécessaire) ; scripts legacy convertis : `public/trans/action_trans_chantier.php`, `public/trans/V2/action_trans_chantier.php`, `public/trans_production/trans_production_valider.php` et `public/modifier_code_chantier_QB.php` (leurs `INSERT` remplissent les ids et toujours les codes). Un renommage de code chantier n'a plus à réécrire le pivot.

**Tests de l'API transfert** (`tests/Feature/ApiTransfert/`, ajoutés 2026-09-24) : une classe PHPUnit par fiche (`<Fiche>TransfertTest`, ordre du tableau de `docs/api_transfert_bhp.md`) + `GeneriqueTransfertTest` (401/404/casse/`tracabilite_envoi`). Toutes héritent d'`ApiTransfertTestCase`, qui fournit les 13 scénarios standard (GET liste/code, POST création/mise à jour/liste/validation, DELETE ok/inconnu/sans droit, traça BHP `BHP_TEST - Brody` + horodatage) et crée un vrai token Sanctum ; chaque fiche déclare sa config (`$table`, `$colonneCode`, `$droitsProfil`, `payloadValide()`) et ajoute ses scénarios propres (liaisons imbriquées, centrales, blocages de suppression...). Base de dev + `DatabaseTransactions` (rollback). Lancer : `php artisan test --filter=<Fiche>TransfertTest` ou tout le dossier `php artisan test tests/Feature/ApiTransfert`. Les tests en échec à leur ajout le 2026-09-24 documentaient de vrais bugs applicatifs (type_vehicule, formule, famille_beton_echantillon, mode_reg, position_compte, zone, et `resoudre_centrales()` en mode local pour client/chantier — colonne `centrale_code` ambiguë), corrigés depuis (voir `docs/api_transfert_bhp.md`). `ChantierTransfertTest` a été adapté le 2026-10-05 au pivot par ids (+ `test_post_rattache_les_centrales_par_id`), à relancer une fois la migration appliquée.

---

## 6quater. Impression d'un lot de fiches par no_exec (`impression_pdf_controller`)

Route API (`POST /impression_pdf/imprimer`, sans middleware d'auth, ajoutée
le 2026-09-11, adaptée le 2026-09-14 au format d'un appelant externe —
automate de centrale) qui reçoit un **tableau JSON** de fiches, chacune avec
`fichier_type` (`0` = bl, `1` = pesee), `fichier_no_exec` (→ `t_bl.bl_no_exec`),
et `fichier_id`/`centrale_code` (reçus, non exploités pour l'instant —
`centrale_code` ne filtre pas la recherche). Chaque fiche est traitée
indépendamment et le contrôleur renvoie un tableau de résultats dans le même
ordre (statut HTTP `200` global, succès/échec porté par fiche). Pour
`fichier_type=0` : retrouve le BL, régénère son PDF dématérialisé via
`App\Services\bl_demat_service::genererPdfPourBl()` (§ ci-dessus) puis
l'imprime via `App\Services\impression_pdf_service::imprimerPdf()` (appel CLI
à SumatraPDF via `Illuminate\Support\Facades\Process` — plus `exec()` brut —
borné à **30 s** avec `Process::timeout(30)`, chemin lu dans
`env('SUMATRA_PATH')`, imprimante par défaut dans `env('IMPRIMANTE')` ; si
SumatraPDF ne répond pas dans ce délai — imprimante introuvable/hors ligne,
boîte de dialogue Windows bloquante — le process est tué et un message
d'erreur dédié est retourné au lieu de bloquer la requête indéfiniment, comme
c'était le cas avant ce correctif du 2026-09-14). Pour `fichier_type=1` : résout l'`id_bl`
correspondant (`bl::where('bl_no_exec', ...)->value('id_bl')`), génère le PDF
des pesées via `App\Services\pesee_pdf_service::genererPdfPourBl()`
(enregistré dans `storage/app/public/pdf_pesee/{id_bl}.pdf`) puis l'imprime de
la même façon. `pesee_pdf_service` a été extrait de
`pesee_controller::imprimer_pdf()` (qui construisait son PDF `Html2Pdf` en
ligne pour le renvoyer en téléchargement navigateur, mode `'D'`) afin d'être
**partagé entre `pesee_controller`** (méthode `construireHtml2Pdf()`, mode
`'D'` inchangé) **et `impression_pdf_controller`** (méthode
`genererPdfPourBl()`, mode `'F'` = enregistrement disque + impression) — même
logique d'extraction que pour `bl_demat_service`, à réutiliser si un futur
type de fiche imprimable apparaît. Voir
[`docs/impression_pdf_controller.md`](docs/impression_pdf_controller.md) pour
le détail des paramètres/réponses. `bl_no_exec` n'a pas de contrainte
d'unicité en base (incrémenté globalement via `bl::max('bl_no_exec') + 1`),
point à surveiller si des doublons apparaissent un jour.

**PDF des pesées V2** (ajouté le 2026-10-23) : bouton « PdfV2 » à côté de « Pdf »
sur `pesee/index.blade.php` → route `pesee.imprimer_pdf_v2` →
`pesee_controller::imprimer_pdf_v2()` → `App\Services\pesee_pdf_service_v2`
+ vue `pesee/pdf_v2.blade.php`. Reprend la logique de calcul de l'ancienne
classe VB6 `ImpPesee.cls` : une colonne par produit (pas de doublon sur dosage
manuel), repli sur `t_ajout` pour les produits absents de `t_produit` (type
« AUTRE2 », kg ; « AUTRE » si introuvable), masse volumique pondérée par
`produit_densite` (adjuvants en litres) et hors ajouts, totaux/moyennes divisés
par le volume réellement exécuté des gâchées, décimales alignées sur l'ancien
programme. La V1 (`pesee_pdf_service`) reste inchangée et **reste celle utilisée
par l'impression automatique** (`impression_pdf_controller`). Détail dans
[`docs/pesee_pdf_v2_calcul_impPesee.md`](docs/pesee_pdf_v2_calcul_impPesee.md).

---

## 6quinquies. Création du BL de cycle (`depart_cycle_service`)

`App\Services\depart_cycle_service::creerBl()` (extrait le 2026-09-15 de
`depart_cycle_controller::lancer_cycle_valider()`) reprend à l'identique le
remplissage des ~150 champs du BL, la création des `ligne_bl` (une par
`ligne_commande` de la commande) et la mise à jour de `commande`/`ventilation`
(numéro de séquence), le tout dans un `DB::transaction()` — une erreur en
cours de route (échec de sauvegarde du BL ou d'une ligne) annule maintenant
tout au lieu de laisser un BL orphelin sans lignes, ce qui pouvait arriver
avant ce correctif. Le contrôleur valide désormais `commande_id`,
`ventilation_id`, `depart_cycle_vehicule_id`, `chauffeur_id` (présence +
existence en base) et `depart_cycle_qte` (numérique, `> 0`) via
`Validator::make()` avant d'appeler le service, et renvoie
`{success:false, message, errors}` en cas d'échec — ce format était déjà
attendu par le JS de `resources/views/depart_cycle/index.blade.php`
(`retour.errors[champ]` pour surligner le champ en erreur) mais jamais
réellement produit par le contrôleur jusqu'ici (chemin d'erreur mort côté
front). L'appel à `envoyer_socket()` (trait qui construit lui-même une
`response()->json()`, cf. §7) reste dans le contrôleur plutôt que dans le
service, pour ne pas faire dépendre la couche service de ce trait HTTP.

---

## 6sexies. Checksum sur le transfert des formules (`public/trans/`)

Ajouté le 2026-09-29, détail dans [docs/transfert_formule_checksum.md](docs/transfert_formule_checksum.md).

Contrôle de cohérence entre la trame formule et ses lignes de composition. **Checksum** = somme des `composition_quantite` de la variante active (formule + centrale), en millièmes (`round(q*1000)` par ligne), complétée par des zéros à gauche sur 10 caractères. Les fonctions communes sont dans `public/inc/fonction.inc.php` : `calculer_checksum_composition()`, `formater_checksum_composition()` et `propager_formule_groupe_centrale()`.

- **Web → centrale** : `action_trans_formule.php?action=extraction` ajoute la clé `formule_checksum` en **dernière position** du JSON. C'est le programme centrale qui fait la vérification.
- **Centrale → web** : en trois temps.
  1. L'en-tête formule arrive avec le paramètre `checksum`, stocké dans `t_formule_detail.formule_checksum_attendu` avec le statut `formule_checksum_statut = 'ATTENTE'`.
  2. Les lignes de composition sont enregistrées pour la centrale source uniquement.
  3. `action_trans_formule_composition.php?action=controle_checksum` compare le checksum attendu à celui recalculé.
     - OK : recopie vers les centrales du même `groupe_centrale_formule_id` et une seule `t_tracabilite_envoi` par centrale.
     - KO : statut `KO`, `erreur=1`, **aucune recopie ni renvoi vers les autres centrales**.
- **Compatibilité** : sans paramètre `checksum` (ancien programme centrale, import CBAO `public/cbao/integrer_cbao.php`), le fonctionnement historique est conservé. La recopie vers le groupe se fait alors ligne par ligne, avec une `t_tracabilite_envoi` par ligne, et les compositions du groupe sont supprimées dès la réception de l'en-tête.

---

## 6septies. Modification groupée de formule — routes manquantes + mode « ajout produit/dosage »

Ajouté le 2026-09-29, détail dans [docs/formule_modification_groupee.md](docs/formule_modification_groupee.md) (documentation complète du contrôleur : routes, droits, 4 actions, modes modification/ajout/suppression de `valider()`).

- **Routes absentes de `routes/web.php`** : la feuille `formule_modification_groupee` (contrôleur, vue, entrée de menu déjà en place depuis le 2026-09-07 selon l'historique de ce document) n'avait en réalité **aucune route déclarée** — la page renvoyait 404. Corrigé par l'ajout du groupe `Route::prefix('/formule_modification_groupee')->name('formule_modification_groupee.')->...` (5 routes : `index`, `liste_produit`, `liste_produit_substitution`, `table`, `valider`), sur le modèle de `groupe_centrale_formule`. À noter pour la suite : la mention "routes ... `formule_modification_groupee`" dans l'historique du 2026-09-07 ci-dessus était donc inexacte — leçon pour ce document : une route citée comme "ajoutée" mérite d'être vérifiée dans `web.php`, pas seulement déduite de la présence du contrôleur/vue.
- **Nouveau mode « Ajouter un produit et un dosage » (`action=3`)** : jusqu'ici la feuille ne permettait que 3 modes de *remplacement* de produit existant. Le contrôleur contenait déjà une logique d'ajout (« MODE 2 : AJOUT/CREATION » dans `valider()`) mais inatteignable depuis l'UI, car `data_table()` exigeait la sélection d'un produit déjà présent pour lister la moindre formule. En mode ajout, ce filtre obligatoire est levé et la contrainte `composition_type` sur la requête de base également, pour lister les formules d'une centrale même si elles ne possèdent pas encore le type de produit à ajouter.
- **Droit dédié** `t_profil.profil_formule_modification_groupee_ajout` (booléen, migration `2026_09_29_1200_V905000_NB_...`), indépendant du droit global `profil_outil_formule` qui gère l'accès à la feuille — même pattern que `profil_devis_comptant_uniquement` (§4). Vérifié côté serveur à 3 endroits (affichage de l'option dans `index()`, `data_table()`, `valider()`) plutôt que côté client uniquement, pour rester robuste à une requête forgée.

---

## 6octies. Facture d'acompte et lignes de facture (module `facture_laravel`)

Ajouté le 2026-09-30, détail dans [docs/facture_acompte.md](docs/facture_acompte.md) et [docs/profil_facture_defacturer_bl.md](docs/profil_facture_defacturer_bl.md).

- **Facture d'acompte** : une facture normale (même numérotation, `facture_avoir` = 0) avec `t_facture.facture_acompte` = 1, créée depuis l'écran de création manuelle (choix « Facture acompte »). Le titre de la fiche indique « Facture », « Avoir » ou « Facture d'acompte ».
- **Déduction** : sur une facture normale, le bouton « + Factures d'acomptes » liste les acomptes non déduits du client, puis `facture_controller::deduire_acomptes()` ajoute en fin de corps deux lignes vides (`E`) puis une ligne `S` par acompte et par taux de TVA. Chaque ligne porte l'article `t_parametre.parametre_article_deduction_acompte_id`, le libellé « Déduction acompte N° du JJ/MM/AAAA » et le HT de l'acompte en négatif. L'acompte est marqué `facture_acompte_deduit` = 1 avec `facture_acompte_num_facture_deduite`.
- **Lien ligne → acompte** : il n'y a pas de clé étrangère. Le lien est le **libellé initial** de la ligne (`ligne_facture_libelle_initial`, non modifiable depuis la fiche pour une ligne de service), relu par `numero_acompte_ligne_deduction()`. La suppression de la ligne, ou de la facture, libère l'acompte. À garder en tête si on modifie un jour ce libellé ou si l'on permet de l'éditer.
- **Ajout de ligne Laravel** : `valider_ligne()` est un portage de `public/facture/valider_ligne_facture.php` (droits, type de ligne et TVA selon l'article, insertion sous la ligne sélectionnée, doublons). La recherche d'article **réutilise les pages legacy** `public/facture/recherche_produit/*.php`, qui écrivent directement dans les champs de la fiche (mêmes `id` DOM). Toute modification de ces `id` dans `facture/form.blade.php` casse la recherche. L'unité renvoyée par ces pages (code ou libellé au lieu de l'id) est traduite côté Laravel par une surcharge de `$.valHooks.select` limitée à `#unite_ligne_facture`.
- **Fenêtres dans la fiche** : `#affiche_facture` a un `transform` CSS (`style.css`), qui rend `position: fixed` relatif au cadre. Les fenêtres ouvertes depuis la fiche (acomptes, recherche produit) sont donc déplacées dans le `body` au chargement et supprimées à la fermeture (`facture/index.blade.php`). Même piège à prévoir pour toute nouvelle fenêtre dans une fiche chargée dans un cadre `#affiche_*`.
- **Droit** `t_profil.profil_facture_defacturer_bl` : le bouton « Défacturer BL » demande ce droit **et** un droit de modification facture (3, 4 ou 5), contrôlé aussi côté serveur. Laravel uniquement : la fiche legacy garde l'ancienne règle (profil 5).
- **Liste des factures** : le filtre « Facture acompte » s'ajoute à Factures et Avoirs. Les types cochés se cumulent, et « Factures » n'inclut plus les acomptes.

---

## 6nonies. Copie de formule entre centrales (`formule_copie`)

Ajouté le 2026-10-03. Portage Laravel de l'écran legacy, renommé `public/formule_copie_default/` (ses URLs internes pointent vers ce nouveau dossier, il reste utilisable). L'URL `/formule_copie`, et donc l'entrée de menu « Copier Formule » de `haut.inc.php`, mène désormais à la version Laravel.

- **Fichiers** : `formule_copie_controller` (`index`, `data_table`, `valider`), vue `resources/views/formule_copie/index.blade.php`, groupe de routes `/formule_copie` (noms `formule_copie.`) dans `web.php` (juste après `formule_modification_groupee`).
- **Droit** : `t_profil.profil_outil_formule` ≠ 0, vérifié dans `index()` et `valider()`.
- **Copie** : `copier_formule_centrale()` (privée) reprend la fonction legacy du même nom (`public/inc/fonction.inc.php`). Pour la centrale cible, elle supprime `t_composition`, `t_formule_detail` et `t_formule_centrale`, puis recopie le détail et la composition de toutes les variantes depuis la centrale de référence (`INSERT … SELECT` via `insertUsing`). Chaque formule est traitée dans une transaction, puis tracée avec `ecrire_traca_std('Copie Formule', code, 'Modification')`. La traçabilité de composition (`tracabilite_compostion_fonction::ecrire_traca_composition`) est écrite dans la même transaction, pour la centrale cible : une ligne « Supprimer ligne » par composition écrasée, puis une ligne « Création » par composition copiée, avec sa variante.
- **Colonnes copiées** : constantes `COLONNES_DETAIL` / `COLONNES_COMPOSITION`, alignées sur `helpers/Modele_helpers.php`. Elles diffèrent de la version legacy, qui copie aussi `formule_cible_wattmetre_minimum/maximum` et `compostition_correcteur`. Ces colonnes manquent dans le helper et dans la base locale (le `INSERT` legacy échoue alors sans bruit). Si elles sont un jour ajoutées pour de bon, il faut les ajouter aux constantes.
- **Retours JSON** : `valider()` renvoie `['success' => bool, 'message' => texte brut]`, comme les autres contrôleurs. La vue affiche le message dans `#message` (vert) ou `#message_base_erreur` (rouge), deux blocs déjà présents dans `base.blade.php`.
- **Ajout par rapport au legacy** : la copie est refusée quand la centrale de référence et la centrale cible sont identiques. Sans ce contrôle, la formule de référence serait supprimée avant d'être recopiée.

---

## 6decies. Fiche client : mini suivi commercial (Synthèse, badges d'alerte, historique, devis)

Ajouté le 2026-10-18, détail dans [docs/client_synthese_commerciale.md](docs/client_synthese_commerciale.md).

- **Fichiers** : `client_controller::synthese()`, route `GET /client/synthese` (nom `client.synthese`, paramètres `client_id` et `annee` facultatif), onglet `#synthese_client` dans `client/form.blade.php`, placé juste avant « Devis » et affiché seulement pour un client existant.
- **Chargement** : en AJAX à la première ouverture de l'onglet (`shown.bs.tab`), puis à chaque changement d'année. L'ouverture de la fiche n'est pas ralentie. Lecture seule, aucune migration.
- **Règle de CA** : **toutes** les factures, validées en comptabilité ou non (`facture_validee` n'est plus filtré depuis le 2026-10-19, comme dans les statistiques standard de CA ; même règle pour les factures échues et les badges d'alerte), avoirs en négatif (`CASE WHEN facture_avoir = 1 THEN -1 ELSE 1 END`). Les acomptes sont comptés à leur émission, puis neutralisés par la ligne de déduction négative de la facture finale (§6octies). **Un volume n'est jamais négatif** : les volumes (carte, tops, prix moyen, volume sur 3 ans) ne comptent que les factures hors avoirs (`$volume`), un avoir corrigeant un montant et non le volume livré. Les montants, eux, restent signés (`$signe`). Toute nouvelle statistique de CA client devrait suivre cette règle pour donner les mêmes chiffres.
- **Comparaison N / N-1** : pour l'année en cours, cumul du 01/01 à aujourd'hui comparé à la même période N-1. Pour une année passée, année complète.
- **Autres indicateurs** : encours repris de `#encours_total` (onglet Général, factures + BL non facturés), jauge sur `client_credit_autorise` / `client_credit_alarme`. Factures échues non réglées (`facture_reste_reglement` > 0, `facture_echeance` dépassée). Taux de transformation des devis sur 12 mois glissants (`devis_etat` 1 sur 1, 2 et 4). Date de la dernière livraison (BL non annulés). Dernière et prochaine action (`t_action`).
- **Répartition du CA (donut)** : calculée sur les **lignes** de facture groupées par `ligne_facture_type` (F béton, G granulat, D adjuvant, J ajout, S service, Z transport ; B/H sous-totaux, C commentaires et E vides exclus), avec le même signe pour les avoirs. ⚠️ Les colonnes `t_facture.facture_montant_beton/granulat/service/transport/autre` existent mais ne sont pas alimentées : ne pas s'en servir pour une ventilation du CA.
- **Top 5 chantiers (CA et volume)** : mêmes lignes, groupées par `t_ligne_facture.chantier_id` (chaque ligne porte son chantier, alors qu'une facture de relevé peut en regrouper plusieurs). Volume = quantité des lignes F (m³), ou des lignes G (t) si le client n'a pas de béton sur la période. Lignes sans chantier exclues.
- **Prix moyen mensuel (courbe)** : HT des lignes F / quantité des lignes F (€/m³), N et N-1, plus un « tout compris » N (HT de toutes les lignes tarifées / même volume). Bascule sur les lignes G (€/t) pour un client sans béton. Mois sans volume = `null` (trou dans la courbe).
- **Volume mensuel sur 3 ans** : mêmes lignes F (ou G), N-2 à N superposées de janvier à décembre, total annuel en légende. ⚠️ `t_facture.facture_nb_beton` des **avoirs** ne correspond pas à leurs lignes béton (des avoirs sans ligne F portent un volume) : une raison de plus d'exclure les avoirs des volumes. Hors avoirs, en-tête et lignes F concordent (2 factures d'écart sur toute la base).
- **Graphique** : Chart.js **2.7.3**, chargé globalement par `public/inc/haut.inc.php`. Il faut donc la syntaxe v2 (`scales.yAxes`, `tooltips`, `Chart.defaults.global`), pas la v3+. L'instance est détruite avant chaque rechargement.
- **Détail des cartes KPI (2026-10-19)** : clic sur CA, Volume, Factures échues ou Devis → modale DataTable, sur le modèle de KPI finance (§6undecies). `client_controller::synthese_detail()` (route `client.synthese_detail`) partage `periode_synthese()` et `factures_echues_query()` avec `synthese()`, pour que le total du détail soit toujours égal à la carte. Toute modification d'une règle de carte doit être reportée dans le `match` de `synthese_detail()`. La modale est déplacée dans le `body` au chargement de la fiche, à cause du `transform` CSS de `#affiche_client`.

**Badges d'alerte de l'en-tête (2026-10-18)**, détail dans [docs/client_badges_alerte.md](docs/client_badges_alerte.md).

- **Affichage** : dans le bandeau noir `#entete`, empilés à droite du titre centré (`#client_alertes`). Un clic ouvre l'onglet visé (attribut `data-onglet`), y compris un sous-onglet, dont l'onglet parent est ouvert d'abord. Le bouton « Basculer tarif » a été déplacé en haut à droite du cadre « Identification ».
- **Sources** : `client_controller::alertes()` (route `client.alertes`) renvoie `{alertes: [{niveau, onglet, texte}]}` pour les factures échues, les devis à relancer (`devis_date_relance` atteinte, même règle que `devis_controller`), l'absence de livraison et l'absence d'action commerciale. Le badge **encours > crédit autorisé** est calculé côté vue, dans le retour de `encours_bls()` (`client_calculer_encours_bl`), pour ne pas refaire ce calcul coûteux.
- **Paramètres** : `t_parametre.parametre_client_alerte_jours_livraison` (défaut 60) et `parametre_client_alerte_jours_action` (défaut 90), 0 = alerte désactivée. Ils se règlent dans l'onglet Client / Chantier des paramètres (migration `2026_10_18_1100_V905001_NB_t_parametre_client_alerte.php`).
- **Requêtes partagées** : `synthese()` et `alertes()` utilisent les mêmes méthodes privées `factures_echues()`, `derniere_livraison()` et `derniere_action()`. Modifier une règle à cet endroit la change dans l'onglet et dans les badges à la fois.

**Historique de la relation et synthèse des devis (2026-10-18)**, détail dans [docs/client_historique_pipeline_devis.md](docs/client_historique_pipeline_devis.md).

- **Sous-onglet « Historique »** (Commercial / Contact / Action, client existant uniquement) : `client_controller::historique()` (route `client.historique`, paramètres `client_id` et `mois`, 0 = tout) renvoie une liste d'événements `{date, type, titre, detail, complement, a_venir}` triée du plus récent au plus ancien. Elle mélange les actions (`t_action`), les devis, la première livraison de chaque chantier (`MIN(bl_date_livraison)` par chantier, BL non annulés) et les incidents (`t_incident`). La frise est construite en JS. Les pastilles de légende servent de filtre par `type` (un clic barre la pastille et masque les événements, filtre conservé au changement de période). Pour ajouter un type d'événement : l'ajouter dans `historique()`, dans `styles_historique` (couleur, icône) et comme pastille `.historique_filtre` avec le même `data-type`.
- **Onglet Devis** (`fiche_annexe/table_devis_fiche_annexe.blade.php`, `devis_controller::data_table_devis_fiche_annexe()`) : colonnes état, quantité prévue et montant HT, plus une clé `synthese` (nombre, m³ et montant HT par état sur la période), lue sur l'événement DataTables `xhr.dt` sans second appel.
- **Règles** : le montant HT d'un devis = somme quantité × prix de **toutes** ses lignes, comme le total de la fiche devis (sous-requête `selectSub` sur `t_ligne_devis`). Les libellés d'état sont centralisés dans `devis_controller::libelle_etat_devis()` (statique), utilisé par la liste des devis, la fiche annexe et l'historique. `t_commande.devis_id` n'est quasiment jamais renseigné : ne pas s'en servir pour mesurer la transformation des devis, utiliser l'état « Gagné ».

**RIB et IBAN : affichés avec des espaces, stockés sans espaces (2026-10-22)**, détail dans [docs/client_rib_iban_formatage.md](docs/client_rib_iban_formatage.md).

- **Affichage** : `formater_rib()` (banque 5, guichet 5, compte 11, clé 2) et `formater_iban()` (groupes de 4, majuscules) dans `client/form.blade.php`, appliqués au chargement de la fiche et au `blur` de `#client_rib` et `#iban`.
- **Enregistrement** : `client_controller::enregistrer()` retire tous les espaces de `client_rib` et `iban`, et les passe en majuscules (`$request->merge`, avant la validation).
- ⚠️ **Le stockage sans espaces est obligatoire** : `public/export_lcr_banque/export_donnees.php` et `public/releve_facture/pdf/releve_facture_vers_pdf.php` découpent `t_client.client_rib` par position (`substr`) sans retirer les espaces (seul `relever_facture_controller` les retire). Tout autre écran qui enregistre `client_rib` (imports xlsx, `public/trans_externe/`) doit aussi le stocker sans espaces. Les RIB déjà enregistrés avec des espaces ne sont corrigés qu'au prochain enregistrement de la fiche.

---

## 6undecies. Feuilles KPI : KPI production et KPI finance

Ajouté le 2026-10-18, détail dans [docs/kpi_finance.md](docs/kpi_finance.md) et [docs/kpi_entetes_style_pesee_bascule.md](docs/kpi_entetes_style_pesee_bascule.md).

- **Deux feuilles**, dans le menu Statistique de `public/inc/haut.inc.php` (le menu est en PHP legacy, pas dans une vue Blade) :
  - **KPI production** : `/kpi`, `kpi_controller`, vue `kpi/index.blade.php`, indicateurs transport et centrale calculés sur les BL. Elle s'appelait « KPI » jusqu'au 2026-10-18 ; seul le libellé a changé, l'URL `/kpi` et les noms de routes `kpi.*` restent les mêmes.
  - **KPI finance** : `/kpi/finance`, `kpi_finance_controller`, vue `kpi/finance.blade.php`. Routes `kpi.finance` (page) et `kpi.finance_donnees` (une seule réponse JSON pour toute la feuille, paramètres `societe_id`, 0 = toutes, et `annee`).
- **Navigation** : `kpi/navigation.blade.php` est inclus en haut des deux vues. Il réutilise `partials/step_navigation.blade.php`, le composant du dashboard, qui accepte maintenant une liste d'étapes `step_navigation_steps` (sans paramètre, il affiche les étapes du dashboard). Pour ajouter une feuille KPI, il suffit d'ajouter une étape dans `kpi/navigation.blade.php`.
- **En-têtes des cartes et titre de la feuille** : en-têtes de carte au style de `pesee_bascule/form.blade.php` (fond `#8cc63f`, texte blanc en gras), limité à `.carte-kpi .card-header` dans chaque vue. Le titre de la feuille (`.titre-kpi`) reprend le style de l'État des centrales : sans fond, vert `#8cc63f`, souligné.
- **Fond de page** : `html, body { background: #f4f6f3; }` dans le `<style>` de chaque vue KPI, même fond que les pages plein écran et l'État des camions (§6duodecies).
- **Règles de calcul de KPI finance** : les mêmes que la Synthèse client (§6decies), pour obtenir les mêmes chiffres. Toutes les factures, validées ou non, avoirs signés en négatif sur les montants mais exclus des volumes, béton = lignes `F`, répartition du CA par `ligne_facture_type`, chantier lu sur `t_ligne_facture.chantier_id`. Le CA par client vient de l'en-tête de facture ; le CA par chantier vient des lignes.
- **Période et comparaison N-1** : choix de l'année comme dans la Synthèse client. Pour l'année en cours, la période va du 01/01 à aujourd'hui ; pour une année passée, c'est l'année complète. Le CA, les avoirs et le volume sont comparés à la même période N-1. Pour les avoirs, les couleurs sont inversées : une hausse s'affiche en rouge. Le graphique du CA mensuel superpose N et l'année N-1 complète. Les factures échues non réglées (reste à régler TTC, échéance dépassée) sont calculées à la date du jour et ne dépendent pas de l'année choisie.
- **Tops chantiers** : libellé « code client / code chantier nom », suivi de « (réf. …) » si `chantier_ref_chantier` est renseigné (rarement : environ 400 chantiers sur 24 500).
- **3 derniers exercices** (prix moyen béton et volume béton mensuels) : `t_exercice` ne contient qu'un exercice pluriannuel (2020-2030), inutilisable pour une comparaison annuelle. `kpi_finance_controller::exercices()` découpe donc des périodes de 12 mois à partir du jour et du mois de début de l'exercice de la société (janvier si « Toutes »). Les 3 exercices sont celui qui contient la fin de la période et les 2 précédents.
- **Graphiques** : Chart.js 2.7.3, chargé globalement (syntaxe v2, cf. §6decies). Les instances sont détruites avant chaque rechargement (fonction `dessiner()` de la vue).
- **Détail au clic** (CA, avoirs, volume, échu) : `kpi_finance_controller::detail()`, route `kpi.finance_detail`, une DataTable dans une modale. Les colonnes changent selon l'indicateur, la table est détruite et reconstruite à chaque ouverture. Le détail et l'indicateur utilisent les mêmes méthodes `periode()`, `facturesQuery()` et `facturesEchuesQuery()`, donc le total du détail est égal à la carte. Pour toute nouvelle règle de filtre, la modifier à cet endroit.

---

## 6duodecies. État des centrales et carte des livraisons

Ajouté le 2026-10-18, détail dans [docs/centrale_etat_connexion.md](docs/centrale_etat_connexion.md) et [docs/carte_livraisons.md](docs/carte_livraisons.md).

### État des connexions centrales (`/centrale/etat_connexion`)

- **Page** : `centrale_controller::etat_connexion()` (vue `centrale/etat_connexion.blade.php`) et `etat_connexion_donnees()` (JSON), routes `centrale.etat_connexion` et `centrale.etat_connexion_donnees` (`auth:sanctum`, `interne`, droit `profil_centrale`). Une tuile par centrale active : état, dernière communication, versions logicielles, m³ et BL du jour. Rafraîchissement toutes les 60 s.
- **Règle de connexion** : constante `centrale_controller::SEUIL_CONNEXION_MINUTES` (3 min), partagée avec `centrale_connectee()` qui alimente l'alerte « Centrales déconnectées » du bandeau (`public/inc/js/centrale_connectee.js`). Au-delà du seuil, ou si `centrale_heure_derniere_communication` est NULL, la centrale est déconnectée. Cette colonne et les versions sont mises à jour par `POST /connexion_centrale` (API, voir §5).
- **Volume du jour** : `SUM(bl_qte_livree)` des BL du jour non annulés. Attention, le dashboard et la carte des livraisons utilisent `bl_qte_livraison`.
- **Accès** : bouton « État des connexions » de la liste des centrales, bouton dans l'alerte du bandeau, et 4ᵉ étape de la navigation du dashboard (`partials/step_navigation.blade.php`).

### Carte des chantiers et toupies du jour (`/suivi_livraison/carte`)

- **Page** : `suivi_livraison_controller::carte()` (vue `suivi_livraison/carte.blade.php`, Leaflet 1.9.4 via unpkg + tuiles OpenStreetMap) et `carte_donnees()` (JSON), routes `suivi_livraison.carte` et `suivi_livraison.carte_donnees` (`auth:sanctum`, `interne`, droit `profil_bl`).
- **Données** : BL du jour non annulés et non préfa, centrales de l'utilisateur (`t_user_centrale`), mêmes exclusions « sous centrale » que `data()`. Un point par chantier livré (volume = `SUM(bl_qte_livraison)`), et pour chaque véhicule son BL le plus récent.
- **Position des toupies** : aucun véhicule n'a de position GPS en base (`Vehicule_Derniere_Position_GPS` vide). La position est déduite du statut calculé par `determiner_statut()` / `etape_atteinte()`, les mêmes règles que l'écran Suivi de livraison : centrale (préparation, chargement, rentrée), mi-chemin (en route, retour), chantier (attente, vidange), non placée (non défini).
- **Coordonnées** : chantiers = `chantier_latitude` / `chantier_longitude` (float, 0 = non géolocalisé, environ 8 700 chantiers sur 24 500 renseignés, valeurs parfois imprécises) ; centrales = **`centrale_latitude` / `centrale_longitude`**, `decimal(10,7)` nullable, ajoutées par la migration `2026_10_18_1500_V905001_NB_t_centrale_latitude_longitude.php` et saisies dans la fiche centrale. `suivi_livraison_controller::coordonnee()` écarte 0, vide et hors bornes. **Sans coordonnées exploitables, la position est estimée d'après l'adresse** par `app/Services/geocodage_adresse_service.php` (géocodage IGN `data.geopf.fr/geocodage`, sans clé, cache Laravel 30 j / 1 j / 10 min, 30 appels max par chargement, rien n'est écrit en base) ; affichée en pointillé avec la mention « Position estimée d'après l'adresse ». Prérequis : `curl.cainfo` configuré dans le `php.ini` du serveur.
- **Accès** : menu Suivi > « Carte des livraisons » (`public/inc/haut.inc.php`) et bouton « Carte » à côté de « Plan de charge » dans les trois plannings legacy (`public/planning/`, `public/planning_prev/`, `public/planning_prev_reel/`).

### État des camions (`/suivi_livraison/etat_camions`)

Ajouté le 2026-10-20, détail dans [docs/etat_camions.md](docs/etat_camions.md).

- **Page** : `suivi_livraison_controller::etat_camions()` affiche la vue `suivi_livraison/etat_camions.blade.php` (layout `base`, avec menus, fond `#f4f6f3` ; le bouton plein écran ne met que la zone de la page en plein écran). Les données viennent de `etat_camions_donnees()` en JSON. Routes `suivi_livraison.etat_camions` et `suivi_livraison.etat_camions_donnees` (`auth:sanctum`, `interne`, droit `profil_bl`). Menu Suivi > « État des camions ».
- **Partial réutilisable** : `suivi_livraison/_etat_camions.blade.php` contient le CSS (préfixé `ec-`), le HTML et le module JS `EtatCamions`, sans `@extends`. Il peut être inclus avec `@include` dans un onglet, à condition de fournir `$centrales` et de ne l'inclure qu'une fois par page. Le rafraîchissement (20 s) ne tourne que si la zone est visible. `EtatCamions.charger()` force une mise à jour.
- **Groupes**, dans cet ordre :
  1. retour (trié par arrivée centrale estimée) ;
  2. vidange ;
  3. attente ;
  4. route ;
  5. chargement (avec « En préparation ») ;
  6. non défini ;
  7. « À la centrale, prochain départ » (pas de BL en cours, mais un tour suivant) ;
  8. inactifs.

  L'ordre est fixé par `suivi_livraison_controller::GROUPES_CAMIONS`.
- **Camions pris en compte** : véhicules présents sur les centrales filtrées, mêmes règles que `planning_laravel_controller::vehicules()` (`t_vehicule_presence_centrale`, sinon la centrale de la fiche véhicule ; `Vehicule_Actif_Beton` et `Vehicule_Definir_Production`). S'y ajoutent les véhicules qui ont un BL du jour. Le statut vient du dernier BL (`determiner_statut()`).
- **Tour suivant** : premier tour de `t_planning` du jour pour ce véhicule, sans `numero_bl` (pas encore produit). Son départ doit être après celui du tour en cours, ou après maintenant − 60 min pour un camion à la centrale. Un tour est non produit si `numero_bl` est NULL, vide ou 0 (`tour_non_produit()`). Si le réel ne donne rien, repli sur `t_planning_prev` : hors blocages et hors OF livrés, et après le dernier départ prévu au réel. La carte affiche alors le badge « prév. ». Requête commune : `requete_tours_du_jour()`.
- **Heures estimées** : `estimer_heure()` prend la dernière étape atteinte du BL et y ajoute l'écart prévu au planning. L'estimation n'est jamais antérieure à maintenant.
- **Marge** : départ suivant − retour estimé, ou − maintenant pour un camion à la centrale. Seuil orange : 10 min (`SEUIL_MARGE_JUSTE` dans le partial).
- **Sélection des centrales** : puces à sélection multiple, comme `planning_laravel` (centrales de production, zone repliable). Paramètre `centrales[]`, aucune cochée = toutes. La sélection est retenue dans `localStorage`.
- **Factorisation** : `requete_bls_du_jour()` et `ids_centrales_filtrees()` (tableau d'ids) sont communes à `carte_donnees()` et `etat_camions_donnees()`.

---

## 6terdecies. Variantes de formule (`formule_variant`)

Introduites en V9.05.000. Documentation complète dans [docs/formule_variantes.md](docs/formule_variantes.md).

- **Principe** : une variante (ex. « Été » / « Hiver ») est un jeu de composition global. `t_composition`, `t_formule_detail` et `t_tracabilite_composition` portent un `formule_variant_id` (entier, défaut 0, **sans clé étrangère**). La clé d'une composition est donc formule + centrale + variante.
- **Variante active** : `t_formule_variant.formule_variant_active = 1`, une seule à la fois. `formule_variant::getActive()` la renvoie, ou à défaut la première variante. Elle sert à la production, à l'API de transfert, au trans legacy, à l'import Massia et au checksum.
- **Variante sélectionnée** : liste « Variante » de l'onglet Composition de la fiche formule. Lecture, saisie, calculs (`composition_calcul_trait`), export et PDF de conformité utilisent `formule_variant_id` de la requête, avec repli sur la variante active.
- **Fichiers** : modèle `formule_variant`, `formule_variant_controller`, vues `resources/views/formule_variant/`, routes `routes/web/formule_variant.php`, menu « Variante de formule » (`haut.inc.php`).
- **Création** : uniquement par duplication d'une variante existante (`dupliquer()`), qui copie en `INSERT … SELECT` toutes les lignes `t_composition` et `t_formule_detail` de la source.
- **Activation** : `activer()` bascule la variante active puis lance le job `envoyer_formule_vers_production_apres_change_formule_variant_active` (en file), qui écrit une traçabilité d'envoi pour chaque formule `formule_definir_production = 1`.
- **Suppression** : refusée pour la variante active, réservée au profil formule 5. Les compositions et détails de la variante sont supprimés en PHP (pas de cascade en base), la traçabilité de composition est conservée.
- **À surveiller** : la modification groupée en mode ajout, sans variante choisie, écrit dans la variante `id = 1` codée en dur, pas forcément la variante active.

---

## 6quaterdecies. Plannings Laravel : Réel, Prévisionnel, Réel/Prév (`planning_laravel`)

Ajouté le 2026-10-19. Portage des trois plannings legacy (`public/planning/`, `public/planning_prev/`, `public/planning_prev_reel/`) avec une interface refaite et les mêmes règles métier. Détail dans [docs/planning_laravel.md](docs/planning_laravel.md).

- **Accès** :
  - par l'URL uniquement : `/planning_laravel/{reel|prev|prev_reel}?date=AAAA-MM-JJ` ;
  - **le menu et les boutons pointent toujours vers le legacy**, qui reste la version en service ;
  - le préfixe évite le dossier physique `public/planning/` ;
  - middleware `auth:sanctum` + `interne`, aucun droit de profil spécifique (comme le legacy).
- **Fichiers** :
  - `planning_laravel_controller` et `routes/web/planning_laravel.php` ;
  - vues `resources/views/planning_laravel/` : `layout` pour la mise en page plein écran sans en-tête ni menus (pas `base.blade.php`), `index` pour la page, `script` pour le JavaScript, `blocage` pour la fenêtre de blocage.
- **Calendrier** : FullCalendar v4.3.1 de `public/inc/planning/`, le même que le legacy.
  - Le `packages-premium/timeline/main.js` est **modifié** (rendu des tours codé dans `renderSegHtml`).
  - La version Laravel le neutralise : elle ne passe pas les propriétés `chantier` / `of_barrer` et dessine elle-même le tour dans `eventRender`.
  - Ne pas remplacer ce fichier sans vérifier les deux versions.
- **Ressources** : les véhicules présents à la date (`t_vehicule_presence_centrale`), regroupés par centrale de présence puis par type de véhicule.
  - Les présences manquantes sont créées à l'affichage, même règle que `creer_presence_vehicule_date()` du legacy.
- **Tours** :
  - Réel : `t_planning`, avec traçabilité `t_tracabilite_envoi` « Création » / « Modification » ;
  - Prévisionnel : `t_planning_prev`, avec les blocages (`planning_blocage = 1`) ;
  - Réel/Prév : les deux superposés, en lecture seule.
- **Réutilisation** : le double-clic sur un tour prévisionnel ouvre la planification manuelle existante (`commande_laravel/commande_recherche`, `fiche_recherche=planification_commande_manuel`). La page fournit les éléments `#filtre`, `#rafraichir_planning_prev` et `#message` que cette vue appelle.
- **Écarts volontaires avec le legacy** :
  - requêtes paramétrées ;
  - client et chantier d'un tour réel non produit pris sur la commande ;
  - couleur « heure à confirmer » rétablie ;
  - déplacement d'un tour réel produit refusé côté serveur ;
  - réordonnancement des commandes non repris, car jamais enregistré.
- **À faire avant de basculer le menu** : recette complète dans le navigateur (glisser-déposer, doubles-clics du Prévisionnel), non faite à la livraison.

---

## 6quindecies. Relevés de factures (`releve_facture`)

Module hybride. Détail de la suppression dans [docs/releve_facture_suppression.md](docs/releve_facture_suppression.md).

- **Legacy** : liste (`public/releve_facture/`), génération (`public/generation_releve_facture/`), JS `public/inc/js/releve_facture/releve_facture_order.js`.
- **Laravel** : `relever_facture_controller` et `routes/web/releve_facture.php`, appelés en AJAX depuis l'écran legacy (routes exclues du CSRF dans `VerifyCsrfToken`) :
  - `releve_facture_telecharger_pdf` : PDF d'un ou plusieurs relevés ;
  - `releve_facture_envoyer_mail` : envoi par mail (job `envoyer_mail_releve_facture_job`) à `t_client.client_mail_fac` (« E-mail Facture démat », affiché dans la colonne E-MAIL FAC de la grille) ;
  - `releve_facture_supprimer` : suppression d'un relevé (colonne SUPP de la grille).
- **Lien facture ↔ relevé** : `t_facture.releve_facture_id` (0 = pas de relevé, pas de clé étrangère). La génération ne reprend que les factures à NULL ou 0.
- **Suppression** : en transaction, remet `releve_facture_id = 0` sur les factures du relevé puis supprime la ligne `t_releve_facture`. Les factures peuvent alors être reprises dans un nouveau relevé. Aucune restriction si le relevé a déjà été imprimé ou envoyé ; le numéro supprimé n'est pas réutilisé (`MAX(releve_numero) + 1`).

## 6sexdecies. Duplication d'un chantier (`chantier_controller::dupliquer()`)

Bouton « Dupliquer » de la fiche chantier (ouverte dans la fiche client). Détail dans [docs/chantier_duplication.md](docs/chantier_duplication.md).

- **Route** : `POST /chantier/dupliquer` (`chantier.dupliquer`), appelée en AJAX avec `chantier_id` et `nouveau_code_chantier`.
- **Code** : saisi par l'utilisateur (12 caractères max). Le `chantier_code_complet` suit la règle de la fiche : code client complété par des espaces à 12 caractères, puis code chantier. Refus si ce code existe déjà.
- **Droits** : les mêmes que le bouton « Nouveau » : refusé si `profil_chantier` vaut 0, 1 ou 3.
- **Copie, dans un même `DB::transaction`** :
  - la ligne `t_chantier` (`replicate()`), même `client_id`, `chantier_credit_en_cours` remis à 0 ;
  - les tarifs `t_tarif` du chantier (`INSERT … SELECT`, nouveau `chantier_id`). Les bases `t_tarif_AAAA` et `t_tarif_edition` ne sont pas copiées ;
  - les centrales `t_chantier_centrale` et les formules `t_chantier_formule`.
- **Traçabilité** : feuille `Chantier`, action « Duplication depuis <code d'origine> ». Si le chantier a des centrales, envoi `T_Chantier` comme à l'enregistrement (`Création`, ou `Suppression` si non défini en production).
- **Après succès** : rafraîchissement de la liste des chantiers du client et ouverture du nouveau chantier (`$("#ajouter_chantier").trigger('click', [chantier_id])`).
- **À surveiller** : le modèle `tarif` déclare `$primaryKey = 'id_traif'` alors que la colonne est `id_tarif`. La duplication passe par du SQL direct et n'est pas concernée.

## 6septdecies. Impression PDF du devis (`devis_laravel`)

Flux hybride, aligné sur le legacy `public/devis/devis_afficher_tr.php`. Détail dans [docs/devis_impression_pdf_modele_client.md](docs/devis_impression_pdf_modele_client.md).

- **Modèles** : sous-dossiers de `RACINE_SITE_MODELE_CLIENT/devis` (`devis_controller::lister_modeles_devis()`), proposés dans les menus « PDF » et « Envoi par mail » de la fiche et de la liste des devis.
- **Génération** : `GET App.modeleclientUrl + devis/{modele}/index.php?id_devis=…` (modèle client) → JSON `{nom_pdf}`. Le PDF est écrit dans `RACINE_SITE_MODELE_CLIENT/pdf/`.
- **Pièces jointes** (fiche uniquement) : `POST devis_laravel.pieces_jointes_pdf` fusionne les pièces jointes PDF/images à la fin de ce PDF, **dans `RACINE_SITE_MODELE_CLIENT/pdf/`** (et non `public/storage/pdf/`).
- **Téléchargement** : script legacy `url('devis/pdf/telecharger_pdf.php')?nom_pdf=…`, qui lit puis supprime le fichier. Toujours passer par `url()` : un chemin relatif ne fonctionne que depuis la page legacy.

---

## 7. Patterns transverses (Services / Traits / fonctions)

Trois styles coexistent sans frontière claire :

- **`app/Services/`** : classes orientées objet, responsabilité unique, injectables — le style "cible" (`tarif_service`, `article_tarif_service`, `MailingService`, `generer_peppol_bis3_xml_service`, `structure_client_externe_service`). Le nouveau sous-namespace `App\Services\Massia\` (client HTTP / mapper / service métier séparés, injection par constructeur, docblocks qui tracent les règles métier et leurs limites) est à ce jour l'exemple le plus abouti de ce style — bon gabarit à réutiliser pour la prochaine intégration tierce plutôt que de repartir du legacy `app/fonctions/`.
- **`app/Traits/`** : utilisés ici de façon détournée comme des **actions de contrôleur entières** (`composition_calcul_trait`, `dupliquer_commande_trait`, `envoyer_socket` — ils appellent `response()->json()`, `auth()->user()` directement). Ce n'est pas l'usage classique d'un trait (comportement réutilisable indépendant du contexte HTTP), ce qui nuit à la testabilité.
- **`app/fonctions/`** (namespace `App\fonctions`, minuscule) : classes "sac de fonctions" procédurales héritées d'un style pré-Laravel, portant parfois une logique métier aussi lourde que les Services (ex. calcul de taxes sur 12-19 Ko) sans règle documentée de placement.

**Numérotation des lignes de devis / lignes de taxe (2026-09-30)** : les lignes de taxe d'un devis (`ligne_devis_type_article = 1` et `article_id` égal à l'un des 9 articles taxe/frais de `t_parametre` : `parametre_article_contribution_environ_id`, `_taxe_carbone_id`, `_taxe_energetique_id`, `_ecotaxe_id`, `_frais_fac_beton_id`, `_tgap_id`, `parametre_rep_granulat_1_article_id`, `parametre_contrib_env_granulat_article_id`, `parametre_article_frais_fac_carriere_id`) ne sont plus numérotées `999`. Après chaque ajout, modification, suppression, déplacement ou duplication, `renumeroter_lignes_devis` renumérote 0..n-1 : les **commentaires (type 9) gardent leur position** (placement libre, y compris entre/sous les taxes), et sur les autres positions les lignes normales passent d'abord puis les taxes. `reserver_numero_ligne_devis($devis_code, 'normale'|'taxe'|'commentaire')` donne le numéro d'une nouvelle ligne et décale les suivantes : normale → après la dernière ligne normale, taxe → après la dernière taxe, commentaire → tout en dernier. Deux implémentations identiques : méthodes de `ajout_lignes_taxes_fonction` (Laravel) et fonctions `renumeroter_lignes_devis($pdo, …)` / `reserver_numero_ligne_devis($pdo, …)` / `est_ligne_taxe_devis()` / `est_ligne_commentaire_devis()` dans `public/inc/fonction.inc.php` (legacy `public/devis/*`). `monter_ligne_devis.php` / `descendre_ligne_devis.php` empêchent une taxe de passer au-dessus d'une ligne normale (les commentaires ne sont jamais bloqués). Une ligne encore à `>= 999` (ancienne donnée) est traitée comme une taxe. Migration de reprise : `2026_09_30_1123_V905000_BAM_t_ligne_devis_renumeroter_lignes_taxes.php`.

Le trait `champs_invisibles_api_si_exterieur_trait` (masquage de champs API interne/externe, utilisé dans `BaseModel`) est défini et exposé sur tous les modèles, mais **aucun consommateur** (Resource API, middleware) n'a été trouvé dans l'échantillon exploré — à vérifier avant de le documenter comme actif.

---

## 8. Bilan : ce qui va bien / ce qui est à surveiller

### Points forts
- **Conventions de nommage très régulières** sur 140+ modules (contrôleur/modèle/vue/table alignés) : une fois le pattern connu, on retrouve n'importe quel module en quelques secondes.
- **Modélisation métier riche et conforme au domaine** (normes béton NF EN 206 : classes de résistance, exposition, chlorure, consistance...) — un vrai savoir métier est capitalisé dans le schéma de données.
- **Documentation utilisateur intégrée** (`views/doc_utilisateur/*`) par module — pratique rare et précieuse pour l'onboarding/support, à conserver et étendre.
- **`fiche_annexe/`** : un vrai mécanisme de composant Blade réutilisable et paramétrable, îlot de bonne factorisation dans un océan de vues monolithiques.
- **Gestion asynchrone du mail correcte** : Jobs + Listeners + hook `Queue::failing()` qui repropage l'échec vers le statut métier en base — un vrai souci de fiabilité.
- **Effort de conformité réglementaire réel** (PEPPOL/UBL2.1) et **couverture de tests Feature** sur plusieurs modules métier sensibles (facturation, produit, service, zone, chauffeur) en plus des tests Jetstream par défaut.
- **Traçabilité version/auteur dans les migrations récentes** : intention louable de savoir quelle migration correspond à quelle livraison.
- **Le module `bl` vient d'être redécoupé en onglets** (`bl_onglet_*`) et **la nouvelle intégration Massia suit un vrai style Service découplé** (client HTTP / mapping / logique métier séparés, règles de correspondance documentées avec leur source) — deux signaux concrets que les recommandations de la précédente version de ce document sont suivies d'effet.

### Points de vigilance
- **Secrets en clair dans le code versionné** (clé OpenAI, mots de passe FTP/SFTP) — **toujours présents et inchangés au 2026-09-07** (vérifiés à nouveau : `gpt_controller.php:17`, `config/filesystems.php:42,53,66`) — le point le plus urgent, indépendant de toute réécriture de code, et d'autant plus critique si ce dépôt est poussé sur un remote partagé/GitHub.
- **Travail non commité qui casse silencieusement le module `bl` s'il est perdu** : `resources/views/bl/*` (19 fichiers, dont la nouvelle décomposition en onglets) est untracked alors que `bl_controller.php` en dépend directement — aucune protection Git dessus pour l'instant.
- **Intégration Massia livrée "à moitié branchée"** : code Services/Controllers présent mais aucune route, config ou migration associée — risque d'oubli si une autre tâche prend le dessus avant la finalisation (voir §6ter et §9).
- **Contrôleurs géants** (jusqu'à 73 Ko / plusieurs centaines de lignes par méthode) mélangeant Eloquent, SQL brut, génération PDF et appels externes — coûteux à faire évoluer sans régression.
- **Vues formulaire monolithiques** (jusqu'à 83 Ko sur un seul fichier `form.blade.php`) — un seul fichier porte tout le cycle de vie d'une fiche complète.
- **Protection d'accès incohérente entre routes d'un même module** (middleware appliqué sur l'index mais pas sur les routes d'écriture) — même défaut que celui qu'on vient de corriger sur `/transfert`.
- **Trois styles de "logique métier transverse"** (Services/Traits-actions/fonctions procédurales) sans règle de placement — risque de duplication de règles métier.
- **Mass assignment quasi ouvert** sur des modèles sensibles.
- **Absence de docblocks métier** (relations, colonnes, règles) au-delà du tag généré par l'IDE helper.
- **Dossier `Unit/` de tests quasiment vide** — la logique de calcul (tarification, taxes, composition béton) n'est pas couverte par des tests unitaires isolés, seulement par des tests Feature bout-en-bout sur certains modules.
- **Bug confirmé dans `composition_calcul_trait.php` (switch `$type_addition`)** : deux `case` correspondent tous les deux à "Addition calcaire cat A" (au lieu d'un pour cat A et un pour cat B) — la colonne `*_calcaire_b_na` de `t_norme_contrainte` n'est donc **jamais** sélectionnée par ce calcul, quelle que soit l'addition réellement utilisée sur le produit. Repéré le 2026-09-08, pas encore corrigé (tâche flaggée en session, non traitée).
- **`t_code_norme` contient des données historiques de mauvaise qualité** (20 lignes : doublons `X0`/`XS3`, 4 lignes avec `classe_resistance`/`type_ciment`/`type_addition` vides, pas de couverture systématique des valeurs valides) — une migration (`2026_09_09_1000_V905000_NB_t_code_norme_remplissage.php`) les a rendues reproductibles sur un nouvel environnement mais n'a **pas** nettoyé ni complété ce jeu de données ; à trancher avec le métier avant d'aller plus loin (cf. `docs/t_code_norme_remplissage.md`).
- **Fiche facture Laravel encore adossée au legacy (2026-09-30)** : la recherche d'article passe par `public/facture/recherche_produit/*.php` (SQL concaténé depuis `$_GET`, dépendance aux `id` DOM de la fiche). Le lien entre une ligne de déduction et son acompte repose sur le libellé de la ligne (voir §6octies).
- **Checksum formule livré côté web seulement (2026-09-29)** : le contrôle reste inactif tant que le programme centrale n'envoie pas `checksum` et n'appelle pas `controle_checksum`. En cas de checksum KO, la composition reçue reste enregistrée pour la centrale source (statut `KO` dans `t_formule_detail`) : seule la propagation au groupe est bloquée. Aucun écran web n'affiche encore ce statut (voir §6sexies).

---

## 9. Feuille de route d'amélioration progressive

Pensée pour être appliquée **par petites touches, au fil des besoins**, sans chantier de refonte globale.

**Étape 0 — Corrections de sécurité immédiates (peu de risque, à faire dès que possible)**
1. Révoquer/rotater la clé OpenAI codée en dur dans `gpt_controller.php` (**toujours en clair au 2026-09-07**), la passer en `.env`.
2. Sortir les mots de passe FTP/SFTP/Trakkeo des valeurs par défaut littérales de `config/filesystems.php` (**toujours en clair au 2026-09-07**).
3. Forcer `SESSION_SECURE_COOKIE=true` en production.
4. Restreindre `allowed_origins` dans `config/cors.php` aux domaines réellement utilisés par l'API.
5. Committer (ou au moins sauvegarder) `resources/views/bl/*` avant toute autre manipulation Git sur ce dossier — actuellement le seul filet de sécurité est le disque local.

**Étape 1 — Cohérence des accès (module par module, dès qu'on retouche un module)**
6. Auditer et harmoniser, module par module, l'application du middleware d'auth **au niveau du groupe de routes** plutôt qu'à la seule route `index` (web.php ET api.php) — commencer par les modules les plus sensibles (facture, commande, chantier).
7. Décider explicitement d'un `$fillable` (plutôt que `$guarded` à 2 colonnes) sur les modèles manipulant de la donnée sensible (facture, client, commande, reglement), en commençant par celui qu'on modifie déjà.
8. Documenter/retirer la commande planifiée vide `tache_recuperer_fichier_xml_ftp` pour éviter de croire ce flux opérationnel.
9. Terminer le branchement de l'intégration Massia avant de l'oublier : routes `v1/massia/...` + middleware d'auth dédié, entrée `config/services.php['massia']` + `.env`, migration de la colonne `bl_exporte` sur `t_bl` (voir §6ter).

**Étape 2 — Nettoyage progressif de la couche métier transverse (opportuniste)**
10. Chaque fois qu'un trait "action de contrôleur" (`composition_calcul_trait`, `dupliquer_commande_trait`, `envoyer_socket`) est touché pour un besoin métier, en extraire la logique dans un vrai Service testable, sans dépendance à `Request`/`response()`.
11. Migrer progressivement les fichiers de `app/fonctions/` les plus consultés vers `app/Services/` (commencer par ceux dont la complexité rivalise déjà avec les Services existants, ex. calcul de taxes/transport) — le style adopté dans `app/Services/Massia/` (client/mapper/service séparés, docblocks de règles métier) est un bon gabarit à suivre.
12. Ajouter un test unitaire chaque fois qu'une règle de calcul (tarif, taxe, composition) est extraite en Service — combler petit à petit le dossier `tests/Unit/`.

**Étape 3 — Refactor ciblé des zones les plus lourdes (quand elles doivent être modifiées de toute façon)**
13. Découper les contrôleurs les plus gros (`commande_controller`, `formule_controller`, `client_controller`, `facture_controller`) en sous-contrôleurs ou Services par sous-fonction, au moment où une évolution touche déjà cette zone — pas de réécriture préventive.
14. Découper les formulaires Blade monolithiques (`client/form.blade.php` et équivalents) en composants Blade par onglet, en profitant du pattern `fiche_annexe/` déjà en place comme modèle de réutilisation — **le module `bl` vient de le faire** (`bl_onglet_*`, non commité, voir §3) : une fois validé et committé, il peut servir de référence concrète pour les prochains modules plutôt que `fiche_annexe/` seul.
15. Pour les nouvelles migrations, conserver la traçabilité version/auteur mais dans le corps du fichier (commentaire/docblock) plutôt que dans le nom, pour revenir au format standard `YYYY_MM_DD_HHMMSS_nom.php` et retrouver la compatibilité totale avec l'outillage Laravel — sans renommer l'historique existant.

---

## 10. Où chercher quoi — aide-mémoire rapide

- **Une route ne répond pas comme attendu ?** → `routes/web/<prefixe>.php` (front métier, un fichier par groupe, voir `docs/routes.md`) ou `routes/api.php` (API), puis le contrôleur `<domaine>_controller.php` correspondant, méthode `index/data_table/afficher_formulaire/enregistrer/supprimer`.
- **Un champ n'apparaît pas dans l'API pour un client externe ?** → vérifier `$champs_invisibles_api_exterieur` sur le modèle concerné et le trait `champs_invisibles_api_si_exterieur_trait`.
- **Un calcul de prix/tarif semble faux ?** → `app/Services/tarif_service.php` et `article_tarif_service.php` (et vérifier qu'il n'y a pas une règle concurrente dans `app/fonctions/`).
- **Un envoi de mail échoue silencieusement ?** → `app/Jobs/`, `AppServiceProvider::boot()` (hook `Queue::failing`), table `t_envoi_mail` (`envoi_mail_statut`).
- **Un problème de communication avec une centrale ?** → `envoyer_socket.php` (échange direct automate) ou `copier_fichier_sftp.php`/commande FTP (échange par fichiers).
- **Une facture électronique PEPPOL est mal formée ?** → `generer_peppol_bis3_xml_service.php` + `generer_data_peppol_par_facture_service.php`, contrôleur `facture_controller::telecharger_facture_peppol()`.
- **Un acompte reste « déduit » à tort, ou n'est plus proposé ?** → `t_facture.facture_acompte_deduit` / `facture_acompte_num_facture_deduite`, puis le libellé initial des lignes « Déduction acompte … » de la facture indiquée ; code dans `facture_controller::deduire_acomptes()` / `supprimer_ligne()` (§6octies).
- **Un chiffre de KPI finance ne correspond pas à la Synthèse client ?** → les deux doivent suivre les mêmes règles : comparer `kpi_finance_controller::donnees()` et `client_controller::synthese()` (§6undecies, §6decies).
- **Une centrale apparaît déconnectée à tort ?** → `centrale_heure_derniere_communication` (mise à jour par `POST /connexion_centrale`) et la constante `centrale_controller::SEUIL_CONNEXION_MINUTES` (§6duodecies).
- **Une toupie ou un chantier est mal placé ou absent sur la carte des livraisons ?** → statut du dernier BL du véhicule (`suivi_livraison_controller::determiner_statut()`), puis `chantier_latitude`/`chantier_longitude` et `centrale_latitude`/`centrale_longitude`, puis l'adresse et le cache de `geocodage_adresse_service` (clés `geocodage_*`), et les logs « Géocodage indisponible » (§6duodecies).
- **Un camion est dans la mauvaise bande, ou son tour suivant est faux, sur l'état des camions ?**
  - Le statut vient du dernier BL : `determiner_statut()` / `etape_atteinte()`.
  - Le tour suivant est la première ligne `t_planning` du véhicule sans `numero_bl`, dans l'ordre de `planning_heure_dep_centrale`.
  - Le code est dans `suivi_livraison_controller::etat_camion()` (§6duodecies).
- **Un véhicule absent ou dans la mauvaise centrale sur un planning ?**
  - Les plannings legacy et la version Laravel (§6quaterdecies) n'affichent que les véhicules présents à la date, `vehicule_presence_centrale_present = 1` dans `t_vehicule_presence_centrale`.
  - Ils les regroupent par la centrale de cette présence, et non par celle de la fiche véhicule.
  - Côté Laravel, voir `planning_laravel_controller::vehicules()`.
- **Un chantier dupliqué n'a pas ses tarifs, centrales ou formules ?** → `chantier_controller::dupliquer()` copie uniquement `t_tarif`, `t_chantier_centrale` et `t_chantier_formule` (§6sexdecies).
- **Un droit d'accès API à ajuster ?** → middleware `ability` (`CheckTokenAbility`) et `interne` (`CheckUserInterne`), tokens Sanctum.
- **Une formule n'est pas arrivée sur les autres centrales du groupe ?** → regarder `t_formule_detail.formule_checksum_statut` de la centrale source (`KO` = propagation bloquée volontairement, `ATTENTE` = `controle_checksum` jamais appelé), puis `public/trans/action_trans_formule_composition.php` (§6sexies).
- **Un échange avec Massia (export BL / import formule) ne fonctionne pas ?** → `app/Services/Massia/*` + `app/Http/Controllers/Massia/*` ; vérifier d'abord que les routes/config manquantes ont bien été ajoutées depuis (voir §6ter, module non branché au 2026-09-07).
- **Un ratio maximal d'addition (composition béton) semble faux ?** → `app/Traits/composition_calcul_trait.php` (lit `t_norme_contrainte` via `classe_exposition`) — vérifier en premier le bug connu de `case` dupliqué sur "cat A" (§8) qui empêche la sélection de la colonne calcaire cat B.
