---
suivi: 1210
date: 2026-08-05
sujet: Diagnostic — import Excel étude Fi lotissement (source A)
chantier: foncier / faisabilite / etude_fi
type: diagnostic
statut: poussé
hash: c5593b5f
fichiers:
    - tools/diag/diag_import_etude_fi_1210.php
    - docs/suivi/SUIVI_1210_diag_import_etude_fi.md
---

# SUIVI #1210 — PHASE 1 DIAGNOSTIC (lecture seule)

## PROMPT ENVOYÉ

Import Excel → étude financière lotissement : cartographie ERP.
Source A uniquement (HECTARE / `Etude Fi`). Source B promotion = hors périmètre.
Décisions : mapping par libellé normalisé ; prévisualisation obligatoire ;
dictionnaire `(type_etude, libelle_normalise)` ; pas d’agent IA en boucle ;
charges seules. Aucun code métier / migration / Vue / rebuild / composer.

## Fiches lues

- #1138 (cadrage) — #1139 Étude Fi réservé.
- #1142 (scénarios) / #1145 (grille) — `Scenarios/Show.vue` zone commune.
- Pas de fiche #1139 dédiée (rattrapage #1147).
- Aucune autre fiche concurrente sur `ScenarioEtudeFiService` / `EtudeFiSaisies`.

## OBJET 1 — PhpSpreadsheet

| Check | Résultat (repo local au diag) |
|-------|-------------------------------|
| `composer.json` | **OUI** — `phpoffice/phpspreadsheet: ^5.7` |
| `composer.lock` | **5.7.0** |
| `vendor/phpoffice/` | **Présent et COMMITÉ** (~570 fichiers, ~5 Mo) |
| Classes | `IOFactory` / `Spreadsheet` chargent via autoload |

Extensions (mesure **locale** — à rejouer sur OVH via le script) :

| Ext | Local |
|-----|-------|
| zip | oui |
| xml / simplexml / libxml | oui |
| mbstring | oui |
| gd | NON (non bloquant pour lecture xlsx simple) |
| xsl | NON (optionnel) |

**VERDICT : PhpSpreadsheet DISPONIBLE — pas d’installation à prévoir pour le lot d’implémentation.**

### Alternative ZipArchive + SimpleXML

Possible (xlsx = zip de XML), plus léger, mais : sharedStrings, styles, dates Excel,
feuilles, cellules fusionnées — dette réelle. Vu que la lib est **déjà** dans le repo
(~5 Mo, déjà payés en git), **recommandation : utiliser PhpSpreadsheet**.
Réinventer le parseur n’apporte rien ici.

## OBJET 2 — Modèle étude lotissement (#1139)

### Tables

**`scenarios`** (`ScenarioFaisabilite`) — versionné par dossier (`version`), modes
`lotissement` | `promotion`, gel, programme lié. Une étude Fi = postes + params
rattachés à **un** scénario. Multiples scénarios / dossier possibles (S1, S2…).

**`scenario_etude_fi_postes`** — clé/valeur :

| Colonne | Type | Null | Contraintes |
|---------|------|------|-------------|
| id | bigint | NON | PK |
| scenario_id | FK → scenarios | NON | cascade, UNIQUE avec code |
| code | varchar(64) | NON | catalogue `EtudeFiSaisies` |
| montant | decimal(14,2) | OUI | **seul champ stocké** |
| timestamps | | | |

UNIQUE `sc_etude_fi_sc_code_uq` (`scenario_id`, `code`).

**`scenario_parametres`** — 1:1 scénario : taux base/forcé (notaire, imprévus,
bancaires, honoraires montage/gestion), €/lot (communication, impôts), override
honoraires commerciaux. **Les % Excel vont ici, pas dans les postes.**

### Référentiel postes — vérité ERP

**Il n’y a PAS 40 lignes en base.** Le catalogue PHP
`EtudeFiSaisies::codesLibelles()` compte **26 codes** (migration #1139 : « ~26 postes
saisis »). Les ~40 lignes Excel CHARGES = 26 saisies + lignes % (params) + lignes
calculées (fiche/grille/moteur) + sous-totaux.

#### Liste exhaustive des 26 postes saisis (ordre catalogue)

| # | code | libellé exact ERP | section | stocké |
|---|------|-------------------|---------|--------|
| 1 | `commission_apporteur_ht` | Commission apporteur HT | achat | montant HT |
| 2 | `taxes_zac_pup_pae_ht` | Taxes ZAC / PUP / PAE HT | taxes | montant HT |
| 3 | `taxes_diverses_ht` | Taxes diverses HT | taxes | montant HT |
| 4 | `vrd_terrassement_par_lot` | LOT 1 Terrassement (par lot) | travaux_vrd | **PU / lot** |
| 5 | `vrd_reseaux_humides_par_lot` | LOT 2 Réseaux Humides (par lot) | travaux_vrd | PU / lot |
| 6 | `vrd_reseaux_secs_par_lot` | LOT 3 Réseaux Secs (par lot) | travaux_vrd | PU / lot |
| 7 | `vrd_espaces_verts_par_lot` | LOT 4 Espaces Verts (par lot) | travaux_vrd | PU / lot |
| 8 | `vrd_compteurs_montant_unitaire` | Compteurs — montant unitaire | travaux_vrd | PU |
| 9 | `vrd_compteurs_nombre` | Compteurs — nombre (saisi) | travaux_vrd | **quantité** |
| 10 | `vrd_travaux_divers` | Travaux divers | travaux_vrd | forfait |
| 11 | `vrd_fouilles_archeologiques` | Fouilles archéologiques | travaux_vrd | forfait |
| 12 | `vrd_demolition` | Démolition | travaux_vrd | forfait |
| 13 | `vrd_erdf` | ERDF | travaux_vrd | forfait |
| 14 | `vrd_divers_1` | Divers 1 | travaux_vrd | forfait |
| 15 | `vrd_divers_2` | Divers 2 | travaux_vrd | forfait |
| 16 | `ext_geometre_par_lot` | Géomètre (par lot) | honoraires_externes | PU / lot |
| 17 | `ext_architecte_par_lot` | Architecte (par lot) | honoraires_externes | PU / lot |
| 18 | `ext_urbaniste_par_lot` | Urbaniste (par lot) | honoraires_externes | PU / lot |
| 19 | `ext_be_vrd_par_lot` | BE VRD (par lot) | honoraires_externes | PU / lot |
| 20 | `ext_be_hydraulique_par_lot` | BE hydraulique (par lot) | honoraires_externes | PU / lot |
| 21 | `ext_be_environnemental_par_lot` | BE environnemental (par lot) | honoraires_externes | PU / lot |
| 22 | `ext_geotechnicien_par_lot` | Géotechnicien et études de sol (par lot) | honoraires_externes | PU / lot |
| 23 | `ext_paysagiste_par_lot` | Paysagiste (par lot) | honoraires_externes | PU / lot |
| 24 | `ext_avocat_forfait` | Avocat (forfait) | honoraires_externes | forfait |
| 25 | `ext_sps_forfait` | SPS (forfait) | honoraires_externes | forfait |
| 26 | `ext_huissier_forfait` | Huissier (forfait) | honoraires_externes | forfait |

Pas d’`id` référentiel en table : le code string **est** l’identité.

#### Ce que l’Excel % doit viser (params, hors postes)

| Paramètre | Défaut Excel | Usage moteur |
|-----------|--------------|--------------|
| `taux_frais_notaire` | 0,023 | → frais notaire € calculé |
| `taux_imprevus_travaux` | 0,03 | → imprévus € sur LOT1–4 |
| `taux_frais_bancaires` | 0,04 | → frais bancaires € |
| `taux_honoraires_montage` / `gestion` | 0,03 | → honoraires € |
| `communication_groupe_par_lot` | 1500 | € / lot Hectare |
| `impots_fonciers_par_lot` | 100 | € / lot Hectare |

Aujourd’hui : **pas d’UI scénario** pour forcer ces params (admin enseigne + init).
Importer des % implique d’écrire `*_force` via un chemin à créer / étendre — **question Robin**.

#### Ne jamais importer (calculés)

Sous-totaux sections ; coût achat foncier HT ; coût viabilité ODF ; frais notaire € ;
honoraires montage/gestion € ; imprévus € ; frais bancaires € ; impôts € ;
honoraires commercialisation (barème) ; communication € ; totaux prix de revient ;
marge foncière ; TVA sur marge / totale ; bénéfice ; ratios ; consolidé ;
recettes / nb lots (fiche + grille).

### Moteur / arrondis

- `brick/math` via `App\Support\Foncier\Faisabilite\Dec` — **jamais de float**.
- Divisions : `RoundingMode::HalfUp`, scale 12.
- Affichage : `strippedOfTrailingZeros()` (pas de `round(2)` systématique en sortie).
- Persistence postes : `decimal:2` cast Eloquent.
- Import : parser en string décimale → `Dec::of` / sync string ; éviter float PHP.

## OBJET 3 — Qui écrit aujourd’hui

| Pièce | Rôle |
|-------|------|
| `ScenarioFaisabiliteController::updateEtudeFi` | PUT `foncier.dossiers.scenarios.etude-fi.update` |
| `ScenarioEtudeFiService::syncSaisies` | upsert + delete hors payload + audit |
| Validation | `postes.{code}` nullable numeric pour chaque code catalogue |
| Droits | `authorize('update', dossier)` + **`foncier.prix`** + scénario non gelé |
| Audit | `ScenarioEtudeFiPoste` : `AuditsFieldChanges` sur `montant` ; événement
  `creation` ; résumé `etude_fi` sur le scénario via `FieldAuditService` |

**Recommandation** : réutiliser `ScenarioEtudeFiService::syncSaisies` après merge
preview → payload complet. ⚠️ `syncSaisies` **supprime** les codes absents du
tableau : un import partiel sans merge écraserait / viderait des postes.

Pas de chemin « bulk » distinct. Params : `ScenarioParametresService` (init seulement).

## OBJET 4 — Où brancher l’UI

- Page : `resources/js/Pages/Foncier/Scenarios/Show.vue` (~**1264** lignes) —
  **fichier gros / disputé** (sérialisation #1142/#1145…).
- Section « Étude financière » (~l.1183+) : bouton « Enregistrer l’étude Fi ».
- Proposition : bouton « Importer un Excel » à côté (bleu action principale #1192),
  ouvrant une **modale** de prévisualisation (évite une page + 2e entrée manifest
  lourde). Filet `border-t` dans le corps de section, pas de nouveau cadre.
- Upload réutilisable : `FoncierEntityDocumentService` — disk **`local`**, max **10 Mo**,
  **xlsx déjà accepté** (MIME OOXML / zip). Pour l’import : upload **éphémère**
  (temp) plutôt que PJ dossier — même règles MIME/taille, autre endpoint.

## OBJET 5 — Volumes / écrasement

Script OVH pour chiffres réels. Modèle : **plusieurs scénarios (versions) par dossier**
déjà prévu — option « créer une nouvelle version » est structurellement naturelle.

**Question Robin (ne pas trancher)** : import sur étude déjà remplie →
écraser / compléter les vides / versionner ? Volumes prod en appui après diag OVH.

## OBJET 6 — Dictionnaire (proposé, NON créé)

Table proposée `etude_fi_import_mappings` (nom indicatif) :

| Colonne | Notes |
|---------|-------|
| id | PK |
| type_etude | ex. `lotissement_hectare` / `promotion_envol` |
| libelle_normalise | résultat de la normalisation |
| code_cible | code `EtudeFiSaisies` ou clé param (`taux_frais_notaire`) |
| cible_type | `poste` \| `parametre` |
| colonne_excel | `C` \| `D` \| `H` (ce qu’on lit) |
| provenance | `saisi_manuel` \| `propose_ia` \| `valide_robin` |
| created_by / updated_by | users.id nullable |
| timestamps | |

UNIQUE `(type_etude, libelle_normalise)`.

Normalisation (spec) : trim → collapse espaces → NFD sans accents → MAJUSCULES →
suffixe ` HT` final retiré → `’` → `'`.

Cas de test : `Travaux Divers HT ` ; `Bureaux d'Etudes VRD  HT` ; `Démolition    HT` ;
`Impôts Fonciers` vs `Impôt foncier `.

## OBJET 7 — Découpage lots

| Lot | Contenu | Risque | Dépendance |
|-----|---------|--------|------------|
| A | Migration dictionnaire + modèle + seed manuel 26 mappings | bas | — |
| B | Parseur Excel (PhpSpreadsheet) + normaliseur + preview DTO (API JSON) | moyen | A |
| C | UI modale preview + apply → `syncSaisies` (merge) sur `Show.vue` | **élevé** (gros Vue + manifest) | B — **seul** lot front |
| D (opt.) | Import % → `scenario_parametres.*_force` + UI/admin | moyen | décision Robin |
| E (ult.) | Recettes / fiche / grille | hors scope | — |

Jamais 2 lots front parallèles sur `Show.vue` / `manifest.json`.
Promotion ENVOL (#1141) : ne pas toucher.

## DÉPLOIEMENT-TEST

Diag OVH (lecture seule) :

```
php tools/diag/diag_import_etude_fi_1210.php
```

Pas de migrate / journal / build pour ce lot diagnostic.

## LEÇON

- Excel « 40 postes » ≠ 26 codes ERP : séparer saisie / params % / calculés dès le
  mapping, sinon on importera des totaux et on cassera le moteur.
- `syncSaisies` = sync totalisant : l’import doit merger, pas poster un sous-ensemble.
- PhpSpreadsheet déjà vendorisé : le prérequis bloquant est **levé** ; rester sur
  l’autoload existant, pas de `composer` OVH.
