---
suivi: 1659
date: 2026-09-22
sujet: Export PDF du scénario de faisabilité complet
chantier: Foncier / Faisabilité
type: feature
statut: poussé
hash: 46a49f67
fichiers:
  - app/Services/Foncier/ScenarioFaisabilitePdfExportService.php
  - app/Support/Foncier/Faisabilite/SeuilsIndicateursEtudeFi.php
  - app/Http/Controllers/Foncier/ScenarioFaisabiliteController.php
  - resources/views/pdfs/scenario-faisabilite.blade.php
  - resources/js/Components/Foncier/ScenarioKpiBar.vue
  - resources/js/Pages/Foncier/Scenarios/Show.vue
  - routes/web.php
  - tests/Unit/ScenarioFaisabiliteExportPdf1659SourceTest.php
  - tests/Feature/Foncier/FoncierScenarioExportPdf1659Test.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1659_export_pdf_scenario.md
---

# SUIVI #1659 — export PDF du scénario complet

## PROMPT ENVOYÉ

Bouton « Export PDF » à côté d’Enregistrer. PDF de TOUT le scénario
(assiette, fiche, grille, étude + 5 KPI en tête avec couleurs de
seuil). Valeurs enregistrées seulement. Réutiliser le mécanisme PDF
existant. Droits `view` ; sans `foncier.prix` aucun montant ni couleur.
Aucune formule modifiée.

## FICHES LUES

- **#1658 LEÇON** — Enregistrer est sur la rangée des ancres ; le
  bouton PDF se place à sa gauche. AppShell non touché. Rebase
  `origin/main` (`fb2aece5`) avant Vite.
- **#1654 LEÇON** — source unique `seuilsIndicateursEtudeFi.js`.
  DomPDF n’exécute pas le JS : miroir PHP `SeuilsIndicateursEtudeFi`
  + test source qui verrouille les bornes des deux fichiers.
- **#1652 LEÇON** — totaux live non persistés. Export serveur = DB.
  Bouton désactivé tant que `sectionsModifiees.length > 0`.
- **#1639** — `FileNameNormalizer` pour le nom de fichier.
- **#1494 / avis d’imposition** — `Pdf::loadView` + A4 paysage.

Anti-collision : dernier numéro = **#1658**. **#1659 est libre.**
`Show.vue` et `ScenarioKpiBar.vue` en série.

## SYNTHÈSE

### Mécanisme PDF retenu

`Barryvdh\DomPDF\Facade\Pdf` déjà utilisé par
`GrillePrixExportService::downloadPdf` (`app/Services/Commercial/GrillePrixExportService.php:388-391`)
et `AvisImpositionExportService::downloadPdf` (`app/Services/Foncier/AvisImpositionExportService.php:85-94`).
Même appel : `Pdf::loadView(...)->setPaper('a4', 'landscape')` puis
`download()`. Aucune nouvelle dépendance. Synchrone (QUEUE=sync),
pas de binaire externe : compatible OVH mutualisé.

### Structure du document (ordre écran)

1. En-tête : **libellé du dossier** (projet), commune, date,
   référence DF, scénario (`S1 — V1`), mode, statut, badge gelé.
2. Cinq KPI (Résultat consolidé, Bénéfice brut, Consolidé / lot,
   Bénéfice / CA HT, Consolidé / CA HT) avec couleurs de seuil.
3. Assiette — parcelles, surface, maîtrise, total.
4. Fiche foncière — lignes + totaux (libellé repris : le PDF circule).
5. Grille — bande d’indicateurs de section + lots + ligne de totaux.
6. Étude financière — 55 lignes, sous-totaux de rubrique.

### Commune

Source primaire : `dossiers_fonciers.code_insee_commune` (commune
déclarée du dossier, même champ que l’en-tête dossier). Si l’assiette
porte d’autres communes, elles sont listées en note (« Parcelles : … »).
Chaque ligne d’assiette garde sa propre commune.

### Modifications non enregistrées

Bouton **désactivé** tant qu’une section est dirty. Justification :
#1652 recalcule à l’écran sans persister ; l’export lit la DB via
les trois `calculerAffichage` de `showDirect`. Un clic malgré tout
sur l’URL ne peut pas inventer des chiffres non enregistrés.

### Tableaux larges

A4 **paysage**, police 6,5–7,5 px (même ordre de grandeur que
l’export grille commerciale #1494). Colonnes de ventilation des 5
couples TVA **regroupées** (surface cessible + prix TTC + régime
principal ou « Ventilé (n) »). `thead { display: table-header-group }`,
`tr { page-break-inside: avoid }`, numérotation `Page N / M`.

### Couleurs de seuil

Miroir PHP de `seuilsIndicateursEtudeFi.js`, hex Tailwind de
`CLASSES_PALIER_SEUIL`. Test `bornesProvider` partagé avec #1654.
Sans `foncier.prix` : aucun KPI, aucun `€`, aucun hex de palier.

### Nom de fichier

`faisabilite_{reference}_{sN-libelle}_{YYYYMMDD}.pdf` via
`FileNameNormalizer` (#1639). Exemple :
`faisabilite_df-2026-0012_s1-v1_20260922.pdf`. Pas d’espace, pas
de caractère Windows interdit. Le libellé du dossier (accents)
reste dans l’en-tête du PDF, pas dans le nom.

### Garde-fous

Aucun calcul modifié. Trois `calculerAffichage` identiques à
`showDirect`. AppShell, #1641–#1658 tableurs, #1652, #1648/#1649,
#1653 : non touchés. Pas de migration. `overflow-x-hidden` absent.

## DÉPLOIEMENT-TEST

```
git pull origin main
php artisan optimize:clear
php artisan view:clear
php artisan route:clear
php artisan journal:sync
```

Pas de migrate.

Recette : scénario V1 DF-2026-0012, comparer PDF et écran au centime.
Compte tiers **sans GS_ADMIN_ERP** : `foncier.lecture` + habilitation
lecture, **sans** `foncier.prix` pour le masquage ; autre groupe M365
sans habilitation dossier pour le 403.

## LEÇON

- DomPDF déjà là (`Pdf::loadView` + paysage) : ne pas introduire
  une seconde stack PDF.
- Export = valeurs **enregistrées**. Désactiver le bouton si dirty
  (#1652), ne pas poster le brouillon.
- Barème JS #1654 : miroir PHP + test de chaînes, jamais une
  troisième copie des bornes dans le blade.
- Commune d’en-tête = commune du **dossier** ; multi-commune d’assiette
  en note, pas un inventaire silencieux.
- `FileNameNormalizer` pour tout nom d’export (#1639).
- `route:clear` obligatoire (route nouvelle).
- Closures ajoutées : une seule, `static fn (int $b): float => $b / 100`
  dans `SeuilsIndicateursEtudeFi::palier` — pas de `use()`, variables
  du barème locales. Le service d’export n’a **aucune** closure.
