---
suivi: 1635
date: 2026-09-22
sujet: Grille de prix d'un scénario — saisie en ligne, génération en masse, mode et taux de TVA
chantier: module-foncier
type: feature
statut: poussé
hash: 44abd31b
fichiers:
  - resources/js/Pages/Foncier/Scenarios/Show.vue
  - app/Services/Foncier/ScenarioGrillePrixService.php
  - tests/Unit/ScenarioGrilleInlineTva1635SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1635_faisabilite_grille_inline_tva.md
---

# SUIVI #1635 — grille de faisabilité : saisie en ligne, génération, TVA

## PROMPT ENVOYÉ

Grille de prix d'un scénario : saisie en ligne type tableur (plus de panneau),
génération en masse depuis un nombre de lots, et choix du régime de TVA — mode
(sur marge / totale / sans TVA) **et** taux (20 % / 10 %) comme deux dimensions
indépendantes, « TVA sur marge à 10 % » devant être exprimable.

## FICHES LUES

- **#1145** — création de `scenario_lots` : c'est là que les couples de colonnes
  surface/prix par régime ont été posés. Lecture décisive (voir constat).
- **#1143** — `FaisabiliteLotissementMoteur` : tous les calculs de la grille,
  taux issus des paramètres du scénario.
- **#1626** — `scenario_lots`, `est_lot_odf`, `type_lot`, `tranche` ; et
  `REGIMES_TVA_FICHE` appartient à la FICHE (acquisition), pas à la grille.
- **#1632** — sous-lots de `lots_commerciaux` : autre objet, non touché ici.
- **#1173** — création en ligne de la grille de commercialisation : « Tab pour
  naviguer, Entrée pour enregistrer, Échap pour annuler ». Grammaire clavier
  réutilisée.
- **#1634** — barre KPI collante livrée juste avant sur cette page.

## AVANT MODIFICATION (constat)

### Table et colonnes — `scenario_lots`
`Schema::getColumnListing` **n'a pas pu être exécuté** (MySQL local refuse la
connexion, `SQLSTATE[HY000] [2002]`, constat récurrent #1626 → #1633). Colonnes
relevées sur le dump `hectareenvol.sql` (CREATE TABLE L4103-4126) **croisées
avec** `ScenarioLot::$fillable` / `casts()` / `auditedFields()` :

`id`, `scenario_id`, `numero`, `est_lot_hectare`, `est_lot_odf`,
`surface_odf_m2`, `recettes_odf_ttc`, `tranche`, `numero_lot_commercial`,
`type_lot`, **`surface_tva_marge`**, **`prix_ttc_tva_marge`**,
**`surface_tva_marge_social`**, **`prix_ttc_tva_marge_social`**,
**`surface_tva_totale`**, **`prix_ttc_tva_totale`**,
**`surface_tva_totale_social`**, **`prix_ttc_tva_totale_social`**,
**`surface_agricole`**, **`prix_ttc_agricole`**, `created_at`, `updated_at`.
Confirmé : `est_lot_odf`, `type_lot` (dont `terrain_a_batir_odf`, `macro_lot`) et
`tranche` (T1–T8) existent bien. **Pas de `deleted_at`** sur cette table (pas de
SoftDeletes ; les lots sont remplacés en bloc à l'enregistrement de la grille).

### 🔴 Y a-t-il déjà un régime de TVA sur la grille ? OUI — et les 5 couples aussi
C'est le constat qui change tout le lot. `scenario_lots` porte **cinq paires de
colonnes surface/prix**, une par couple mode × taux :

| Couple | Colonnes | Traitement moteur |
|---|---|---|
| TVA sur marge, taux normal | `surface_tva_marge` / `prix_ttc_tva_marge` | `(prix − PR×surface) ÷ (1+t) × t`, `t = tauxTva` |
| **TVA sur marge, taux social** | `surface_tva_marge_social` / `prix_ttc_tva_marge_social` | même formule, `t = tauxTvaSocial` |
| TVA totale, taux normal | `surface_tva_totale` / `prix_ttc_tva_totale` | `prix ÷ (1+t) × t`, `t = tauxTva` |
| TVA totale, taux social | `surface_tva_totale_social` / `prix_ttc_tva_totale_social` | même formule, `t = tauxTvaSocial` |
| Sans TVA | `surface_agricole` / `prix_ttc_agricole` | « Agricole : aucune TVA » (commentaire du moteur, L908) |

**« TVA sur marge à 10 % » était donc déjà stockable et déjà calculée** —
simplement **invisible** : le sélecteur de régime n'existait que dans le panneau
déplié (`row.regime_principal`, `REGIMES_LOT`, `multi_regimes`), jamais dans la
grille. Les taux ne sont pas en dur : ils viennent de
`scenario_parametres.taux_tva` / `taux_tva_social` (defaults 0,20 / 0,10),
résolus par `FaisabiliteParametresResolus`.

**Fiche foncière ≠ grille — à ne pas confondre** : la FICHE porte
`FoncierReferentiels::REGIMES_TVA_FICHE` (`tva_sur_marge`, `tva_totale`,
`sans_tva`) sur `scenario_fiche_lignes`, pour le prix d'**ACQUISITION** ; la
GRILLE porte les 5 couples ci-dessus pour la **VENTE**. Les deux référentiels
restent séparés, aucun n'a été modifié.

### Où la grille est construite
`resources/js/Pages/Foncier/Scenarios/Show.vue`, section « Grille de prix » :
tableau L1305-1330 (avant modification) ; **clic sur la ligne →
`toggleLotExpand(idx)`** qui déplie une ligne-panneau (`expandedLotIdx`) portant
N° commercial, Type, Tranche, cases Hectare/ODF, régime + surface + prix, et la
ventilation multi-régimes. C'est ce panneau que Robin trouve « long ».

### Flag « H »
`row.est_lot_hectare` → **lot Hectare**, c'est-à-dire vendu par l'enseigne, par
opposition à `est_lot_odf` (**ODF**, organisme foncier : le lot est porté par
l'ODF, ses recettes sont saisies en `recettes_odf_ttc` / `surface_odf_m2`). Un
troisième badge `TVA −` signale une TVA sur marge négative. Les trois ont reçu
une infobulle explicite.

### TOTAL TVA affiché en KPI
`grille_prix.totaux.total_montant_tva`, produit par
`FaisabiliteLotissementMoteur::calculerTvaLot()` puis sommé : par lot,
`montant_tva_lot = tva_marge + tva_marge_social + tva_totale + tva_totale_social`
(l'agricole n'apporte rien). La grille **porte donc bien** le régime : le total
n'est pas reconstitué ailleurs.

### Saisie en ligne existante dans l'ERP
- `LotsGrillePanel.vue` (#1173) : création **en ligne** avec la grammaire
  « Tab pour naviguer, Entrée pour enregistrer, Échap pour annuler » —
  grammaire reprise ici.
- `InlineEditableField.vue` (foncier) : édition champ par champ avec **un PATCH
  par champ** — écartée pour une grille de 50 lots (50 requêtes, 50 recalculs du
  moteur).
- Grille de faisabilité : les lignes sont **déjà** un modèle client
  (`lotRows` + `useForm`) envoyé en **un seul PUT**. La saisie en ligne ne
  nécessitait donc aucun nouvel endpoint.

## SYNTHÈSE

### Décision de structure : aucune colonne ajoutée
Le prompt demandait de stocker mode et taux **séparément**. Le schéma existant
encode le couple dans le **nom** des colonnes, ce qui n'est pas « séparé » au
sens strict. Deux options :
1. ajouter `tva_mode` + `tva_taux_code` → **deux sources de vérité** pour la même
   information, et un moteur qui continue de lire les colonnes : le TOTAL TVA
   des scénarios existants aurait dépendu d'une valeur par défaut choisie par
   nous ;
2. garder les 5 couples comme stockage et exposer **mode et taux comme deux
   listes indépendantes** dans l'UI, la paire (mode, taux) désignant le couple.

**Option 2 retenue.** Justification : la correspondance est bijective sur les 5
combinaisons attendues, le moteur n'est pas touché, **aucune migration** n'est
nécessaire et le TOTAL TVA des scénarios existants est **strictement préservé**
— voir ci-dessous. La limite assumée : ajouter un 3ᵉ taux (5,5 %) exigera une
paire de colonnes, alors que deux colonnes dédiées l'auraient absorbé ; c'est le
prix de la non-régression, et le sujet est isolé dans `REGIMES_LOT`.

### 🔴 Valeur par défaut des lots existants et effet sur le TOTAL TVA
**Aucune valeur par défaut n'est écrite, donc le TOTAL TVA est inchangé, au
centime.** Le mode et le taux affichés sont **déduits** de la paire de colonnes
déjà renseignée (`detectRegimePrincipal`, fonction préexistante) : un lot en
`prix_ttc_tva_marge` s'affiche « TVA sur marge / 20 % », un lot en
`prix_ttc_tva_marge_social` « TVA sur marge / 10 % (social) ». Un lot vide
s'affiche « TVA sur marge / 20 % » (défaut historique de `emptyLotRow`) sans rien
écrire tant que l'utilisateur ne saisit pas. Comme aucune migration ne touche la
table et que le moteur lit les mêmes colonnes, **un scénario existant rend
exactement le même total qu'avant le lot**.

### Saisie en ligne
Chaque cellule éditable rend directement son contrôle, lié au modèle local
(`v-model` sur `lotRows`) : **plus aucun panneau ne s'ouvre au clic sur la
ligne**. Colonnes éditables : **Lot** (`numero_lot_commercial`), **Type**
(`type_lot`), **Mode TVA**, **Taux**, **Surf. cessible** et **Prix TTC** (écrits
dans le couple actif).

**Cellules calculées, non éditables** (fond gris `bg-[#F3F4F6]/60`, infobulle
« Calculé (moteur) ») : **#** (`numero`), **€/m²** (`prix_m2`), **TVA**
(`montant_tva_lot`), **Hon. mont.** (`honoraires_montage_lot`), **Hon. gest.**
(`honoraires_gestion_lot`). Les badges Flags (H / ODF / TVA −) restent des
indicateurs.

**Clavier** : `Entrée` valide et descend d'une ligne sur la même colonne, en
changeant de page si la ligne suivante est sur la page d'après ; sur la dernière
ligne, elle valide et reste (aucune erreur, aucune ligne créée). `Tabulation`
reste **native** : l'ordre du DOM étant ligne par ligne, elle va à droite puis
passe à la première cellule de la ligne suivante — exactement le comportement
demandé, sans code. `Échap` rétablit la valeur mémorisée au focus de la cellule.

**Enregistrement : GROUPÉ** (inchangé). Un seul PUT de la grille par
« Enregistrer la grille ». Un appel par cellule aurait signifié, sur 50 lots,
50 requêtes et 50 exécutions du moteur de faisabilité, pour un résultat
identique : inutilisable. Le bouton flottant du #1634 couvre le cas « je suis en
bas de page ».

Le panneau déplié reste accessible par un bouton **« Détail »** explicite, pour
la tranche, les champs ODF et la **ventilation multi-régimes** (cas rare où un
lot porte plusieurs régimes à la fois) — ce que deux listes ne peuvent pas
exprimer.

### Génération en masse
Case **« Nombre de lots »** + bouton **« Générer »** : **ajoute** N lignes à la
suite, **jamais de remplacement** (aucun `lotRows = []`, aucun `splice(0…)` —
vérifié par test). Contenu des lignes créées, annoncé à l'écran : numérotation
automatique à la suite, et reprise du **type**, de la **tranche**, des **flags
Hectare/ODF** et du **régime de TVA** de la dernière ligne (ou des valeurs par
défaut si la grille est vide) ; **surfaces et prix restent vides**, à saisir.
Limite **200 lots** par génération, avec message explicite si dépassée, et
message de confirmation du nombre ajouté.

### Taux affichés
`ScenarioGrillePrixService::calculerAffichage()` expose désormais
`taux_tva: { normal, social }` — les valeurs **réellement utilisées par le
moteur** (paramètres du scénario). Les libellés de la liste Taux sont donc
« 20 % » / « 10 % (social) » calculés, jamais écrits en dur. Même clé ajoutée à
`payloadVide()` pour une grille vide. Aucun calcul modifié.

## TABLEAU DE MAPPING OBLIGATOIRE

| Libellé Mode | Libellé Taux | Valeurs postées | Colonnes stockées (surface / prix) | Libellé back |
|---|---|---|---|---|
| TVA sur marge | 20 % | `mode=marge`, `taux=normal` | `surface_tva_marge` / `prix_ttc_tva_marge` | `tva_marge` — « TVA sur marge » |
| TVA sur marge | 10 % (social) | `mode=marge`, `taux=social` | `surface_tva_marge_social` / `prix_ttc_tva_marge_social` | `tva_marge_social` — « TVA marge social » |
| TVA totale | 20 % | `mode=totale`, `taux=normal` | `surface_tva_totale` / `prix_ttc_tva_totale` | `tva_totale` — « TVA totale » |
| TVA totale | 10 % (social) | `mode=totale`, `taux=social` | `surface_tva_totale_social` / `prix_ttc_tva_totale_social` | `tva_totale_social` — « TVA totale social » |
| Sans TVA | (sans objet, liste désactivée) | `mode=sans_tva`, `taux=null` | `surface_agricole` / `prix_ttc_agricole` | `agricole` — « Agricole » (aucune TVA) |

Les taux 20 % / 10 % sont les **valeurs du scénario**
(`scenario_parametres.taux_tva` / `taux_tva_social`) : si un scénario les
paramètre autrement, les libellés suivent.

## VÉRIFICATIONS

`php -l` : OK (`ScenarioGrillePrixService.php`, journal, test).

Reflection / DI (sortie brute en synthèse) : service grille résolu par le
conteneur et instanciable, taux par défaut 0,2 / 0,1, `payloadVide` portant la
nouvelle clé.

Tests : `ScenarioGrilleInlineTva1635SourceTest` (8 tests) → **8 passed (85
assertions)**, dont un test qui vérifie qu'**aucune migration hors création ne
touche `scenario_lots`**. Non-régression scénario / faisabilité (#1145, #1144,
#1139, #1629, #1634, moteur) → **68 passed, 7 skipped**, avec **1 échec
préexistant hors périmètre** : `FoncierScenarioFaisabilite1142SourceTest > vues
onglet et fiche coquille` attend `'faisabilite'` dans
`resources/js/Pages/Foncier/Dossiers/Show.vue`, fichier non modifié par ce lot,
déjà en échec au #1629.

⚠️ **Aucun test avec base** (`pdo_sqlite` absent) : la saisie réelle, la
génération et l'enregistrement n'ont pas été exécutés contre une base. Les tests
6, 7, 8, 9 et 10 en recette sont indispensables, en particulier la comparaison
du TOTAL TVA avant/après sur un scénario existant.

## DÉPLOIEMENT-TEST

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

**Aucune migration** (c'est le point clé de ce lot). Pas de route. Pas de `npm` /
`composer` sur OVH.

1. Cliquer une cellule : édition immédiate, aucun panneau ne s'ouvre.
2. Entrée : descend d'une ligne ; sur la dernière, valide et reste.
3. Tabulation : va à droite, puis à la première cellule de la ligne suivante.
4. Échap : la cellule retrouve sa valeur d'avant saisie.
5. #, €/m², TVA, honoraires : grisés, non éditables.
6. Saisir 20 dans « Nombre de lots » → 20 lignes ajoutées et numérotées, les
   lots existants intacts. Essayer 500 → message de limite.
7. « TVA sur marge » + « 10 % (social) » sur un lot, enregistrer, recharger :
   conservé.
8. « TVA totale » + « 20 % » sur un autre lot : les deux coexistent.
9. Le TOTAL TVA du bandeau (#1634) reflète les régimes choisis.
10. **Scénario existant : noter le TOTAL TVA avant déploiement et le recomparer
    après — il doit être identique au centime.**
11. Dupliquer et Supprimer fonctionnent toujours ; « Détail » ouvre le panneau
    (tranche, ODF, multi-régimes).

## LEÇON

Avant d'ajouter des colonnes pour « le mode et le taux », lire la migration de
création : ici les cinq combinaisons étaient stockées depuis le #1145, sous forme
d'une paire de colonnes par couple. Ce qui manquait n'était pas le modèle mais
**l'accès** — le sélecteur était enfermé dans un panneau déplié. Ajouter des
colonnes aurait créé une seconde source de vérité et mis en jeu le total TVA de
tous les scénarios existants, pour un besoin déjà couvert.

Une saisie « type tableur » ne demande pas d'endpoint par cellule quand la grille
est déjà un modèle client enregistré en bloc : il suffit de rendre les contrôles
dans les cellules. Et la Tabulation n'a besoin d'aucun code — l'ordre du DOM
ligne par ligne produit exactement le déplacement attendu.
