---
suivi: 1626
date: 2026-09-21
sujet: Diagnostic TVA sur marge et sous-lots grille
chantier: commercialisation / tva
type: diagnostic
statut: poussé
hash: 39108838
fichiers:
  - docs/suivi/SUIVI_1626_diagnostic_tva_marge_sous_lots.md
---

# SUIVI #1626 — diagnostic TVA sur marge / sous-lots

## PROMPT ENVOYÉ

Diagnostic lecture seule. Détailler un lot de la grille (parent = somme
des enfants, régimes TVA mixtes). PR foncier et frais de notaire saisis
à la main (page TVA SUR MARGE, pas le bilan). Surface de référence =
grille hors ODF et hors non constructible. Formules du document PR
16/09/2026. Ne pas coder. Établir grille, hiérarchie, type de zone,
formules, figeage, cadastre, page FINANCIER, découpage et arbitrages.

## FICHES LUES

- **#1062 LEÇON** — lots de grille = `lots_commerciaux` (pas `lots`).
- **#1064 / #1066** — colonnes Surface/SDP/CES ; `regime_tva` saisi ;
  whitelist grille n’écrit pas `prix_vente_ttc`.
- **#1143 / #1145** — moteur faisabilité : `scenario_lots`, types ODF /
  macro-lot, TVA marge/totale/agricole. **Autre objet** que la grille
  commerciale.
- **#1154 / #1161 / #1168** — `prix_vente_ttc` figé à la réservation ;
  pivot `reservation_lots` ; AdF snapshot TTC.
- **#1186 / #1187** — `mode_vente=odf` et statut `dation` ; ODF n’est
  pas un type_logement.
- **#1540 / #1543 / #1568** — `parcelles.constructible` (5 valeurs) et
  `destination` (7 valeurs) : **pas** le type de zone des sous-lots.
- **#1494 / #1158** — totaux bandeau = `StockSyntheseCalculator`.
- **#1622** — onglet FINANCIER (BILAN / BUDGET). TVA SUR MARGE prévu
  comme sous-onglet, pas encore créé. Collision Vite `FinancierSubNav`.
- **#1621 / #1625** — zones différentes ; push Vite en série.

## MÉTHODE SCHÉMA

`Schema::getColumnListing` : **échec local** (MySQL `127.0.0.1:3306`
refusé). Colonnes lues dans le `CREATE TABLE` du dump workspace
`hectareenvol.sql` **et** recoupées aux migrations
`database/migrations/*lots_commerciaux*`. Aucun nom inventé. Le dump
programmes/parcelles peut être légèrement en retard (ex. `destination`
ajoutée #1568, absente du dump).

## SYNTHÈSE

### A. Grille de prix aujourd’hui

**Table** : `lots_commerciaux` (`App\Models\LotCommercial`).
La table `lots` est vestigiale.

**Colonnes exactes (CREATE TABLE dump)** :

| Colonne | Type dump |
|---------|-----------|
| id | bigint unsigned |
| programme_id | bigint unsigned NOT NULL |
| numero_lot | varchar(255) NOT NULL |
| type_logement | varchar(255) NOT NULL |
| etage | varchar(255) NULL |
| surface_habitable | decimal(8,2) NULL |
| surface_terrasse | decimal(8,2) NULL |
| surface_terrain | decimal(10,2) NULL |
| sdp | decimal(10,2) NULL |
| ces | decimal(10,2) NULL |
| exposition | varchar(64) NULL |
| nb_stationnement | int NOT NULL default 0 |
| plan_fichier_path | varchar(255) NULL |
| prix_grille_ttc | decimal(12,2) NOT NULL |
| prix_vente_ttc | decimal(12,2) NULL |
| difference | decimal(12,2) NULL |
| statut | enum en_stock, en_vente, reserve, acte, annule, dation |
| mode_vente | varchar(32) NULL |
| tma_montant_ttc | decimal(12,2) NULL |
| remises_ttc | decimal(12,2) NULL |
| agence_nom | varchar(255) NULL |
| commission_ttc | decimal(12,2) NULL |
| commentaire | text NULL |
| date_acte | date NULL |
| date_reservation | date NULL |
| created_at / updated_at / deleted_at | timestamps |
| regime_tva | enum tva_20, tva_10, tva_55, exonere NOT NULL default tva_20 |

**Absentes** (vérifié dump + fillable + migrations) : `parent_id`,
`lot_parent_id`, `type_zone`, `constructible`, `tva_figee`, `pr_foncier`,
lien parcelle.

Enfants **techniques** (pas des sous-lots TVA) : `lot_emplacements`,
`lot_annexes`, `lot_commercial_historique`, `offres_commerciales`.

**UI lotissement** (`LotsGrillePanel.vue` EXPORT_COLONNES_LOTISSEMENT) :
Lot → `numero_lot` ; Surface → `surface_terrain` ; SDP → `sdp` ;
CES → `ces` ; Prix de Grille → `prix_grille_ttc` ; Prix de Vente →
`prix_vente_ttc` ; Écart → `difference` ; Statut → `statut`.
Le modal d’édition ajoute **Régime TVA** (`regime_tva`).

**Valeurs `regime_tva` exhaustives** (`LotCommercial::REGIME_TVA` + ENUM) :

| Valeur postée / DB | Libellé | Taux accessor `tauxTva` |
|--------------------|---------|-------------------------|
| tva_20 | TVA 20% | 20 |
| tva_10 | TVA 10% | 10 |
| tva_55 | TVA 5,5% | 5,5 |
| exonere | Exonéré | 0 |

**Il n’existe PAS** `tva_sur_marge` / `tva_totale` / `sans_tva` sur la
grille commerciale. Ces trois codes sont `FoncierReferentiels::REGIMES_TVA_FICHE`,
utilisés par la **fiche foncière / scénario de faisabilité**, pas par
`lots_commerciaux`.

**ODF** : sur la grille = `lots_commerciaux.mode_vente = 'odf'`
(`MODE_VENTE` : direct, co_amenageur, odf, dation). Ce n’est **pas**
`type_logement` (T1…Parking…Local commercial).

Sur la **faisabilité** (autre table `scenario_lots`) : `est_lot_odf`,
`type_lot` ∈ TYPES_LOT_SCENARIO (`terrain_a_batir_odf`, `macro_lot`, …),
`tranche` varchar(8) T1–T8 Excel, `numero_lot_commercial` **string**
sans FK.

**Hiérarchie parent/enfant lots commerciaux** : **n’existe pas**.
Le « macro lot social ML13 » du document PR correspond au **type**
`macro_lot` des `scenario_lots` (étude), pas à un parent SQL sur la
grille de vente.

**Totaux bandeau** (`StockSyntheseCalculator`, écran + export #1494) :
périmètre = tous les lots du programme **hors** `annule` et `dation`.
Par statut : count + Σ `valeur_commerciale_ttc`.
Vente totale = Σ valeur des lots actifs.
Avancement lots = (réservé + acté) / total actifs.
Avancement valeur = Σ valeur (réservé + acté) / vente totale.
`valeur_commerciale_ttc` : réservé/acté → `prix_vente_ttc` ou grille ;
en_stock/en_vente → prix effectif (promo ou grille).

Si parent **et** enfants sont des lignes `lots_commerciaux` avec un
statut commercial, **double comptage certain**.

### B. Parent et enfants — deux options, pas de choix

Exemple Robin : parent 100 m² = 1a 90 + 1b 10, 99 000 = 90 000 + 9 000.
Grille actuelle L’AURORE : un **seul** lot 1 à 138 m² (pas encore
découpé). Le 138 n’est pas une somme d’enfants existants.

**Option 1 — parent calculé, non saisi dès qu’il a des enfants**
- Surface / SDP / CES / prix grille du parent = Σ enfants.
- Saisie uniquement sur les enfants. Écart parent/enfants impossible
  par construction.
- Conséquence L’AURORE : le 138 m² disparaît au profit de la somme
  réelle des sous-lots (si elle fait 100, l’affichage passe à 100).
- Totaux : ne compter **que les feuilles** (enfants), jamais le parent.
  Le parent n’a pas de statut vendable, ou un statut dérivé.

**Option 2 — parent saisi, contrôle d’écart**
- Le 138 m² peut rester. Un écart 138 vs Σ enfants est affiché
  (alerte, pas un zéro silencieux).
- Qui est vendu ? Si on vend le parent **et** un enfant : double CA.
- Qui porte la réservation (`reservations.lot_commercial_id` + pivot
  `reservation_lots`) : aujourd’hui **un** `lot_commercial_id` par ligne
  de pivot.

**Vente** (à arbitrer, non tranché) :
- Aujourd’hui pipeline `en_stock → en_vente → reserve → acte` sur
  **une** ligne. `peutReserver` = statut `en_vente`.
- Si vente du **parent tel quel** : les enfants ne sont pas des
  produits ; TVA mixte à l’acte = comment la ventiler dans l’AdF ?
- Si vente des **enfants** : le parent n’est qu’un regroupement
  d’affichage ; chaque sous-lot a son statut / résa / acte.
- Hybride (vendre le parent **ou** les enfants) : explosif pour les
  totaux et les AdF.

**AdF** : `GenerationAppelFondService` fait `montantHt = TTC / 1.20`
**en dur**, ignore `regime_tva`. Introduire TVA sur marge / exonéré
sans figer la TVA à l’acte **casserait** HT/TVA des appels de fonds.

### C. Type de zone

**N’existe pas** sur `lots_commerciaux`.

Ne pas confondre :
- `parcelles.constructible` (#1540/#1543) : oui, pour_partie, potentiel,
  a_devenir, jamais (+ NULL).
- `parcelles.destination` (#1568) : 7 intentions foncières (à définir,
  support d’opération, bouchon, réserve, cession, échange, rétrocession).
- Surface agricole des `scenario_lots` (faisabilité).

**Proposition (non figée)** pour les sous-lots de grille :
constructible | non_constructible — assez pour le dénominateur Robin.
Éventuellement plus tard : voirie, espace vert, agricole (hors lot).

**Dénominateur = 0** : si tous les sous-lots d’un programme (ou tous
ceux hors ODF) sont non constructibles, `surface_ref = 0`.
Le moteur faisabilité met déjà `prix_revient_foncier_m2 = null` si
surface hors agricole nulle, et TVA marge à 0 **avec signal**.
Même exigence ici : **ne pas diviser**, **ne pas mettre 0 silencieux**.

**Tension formule × exclusion** : Robin veut (1) TVA sur marge sur
1b non constructible et (2) exclure le non constructible du
dénominateur. Si 1b est au numérateur (sa surface) mais pas au
dénominateur, la somme des PR lots **dépasse** le PR programme.
Trois issues à arbitrer :
1. 1b exclu aussi du numérateur → PR_1b = 0 → marge = presque tout le TTC.
2. 1b inclus au dénominateur malgré « non constructible » (contredit la règle).
3. Dénominateur = surfaces **constructibles ET en régime marge** seulement
   (1a TVA 20 % hors dénominateur) → si seuls les 1b non constructibles
   sont en marge, dénominateur 0.

### D. Calcul TVA

Deux univers **incompatibles** aujourd’hui :

| | Grille commerciale | Faisabilité foncière |
|---|--------------------|----------------------|
| Table | lots_commerciaux | scenario_lots |
| « Régime » | un **taux** (20/10/5,5/exo) | un **mode** (marge/totale/sans) par **slice** de surface |
| Taux | porté par `regime_tva` | `scenario_parametres.taux_tva` et `taux_tva_social` |

Robin demande les **modes** (marge / totale 20 % / sans) + calcul auto.
Le taux 10 % social existe déjà côté faisabilité, pas comme mode de
grille. **Recommandation** : le régime (mode) **porte** le taux
applicable (20 ou 10), plutôt que de le déduire. L’ENUM actuel ne
suffit pas : il faudrait un mode **et** un taux (ou un code composé).

**Formules** (taux t, TTC, PR_lot) :

- TVA totale : `TVA = TTC × t / (1+t)` ; `HT = TTC − TVA`.
- TVA sur marge : `PR_lot = PR_prog × surface_lot / surface_ref` ;
  `TVA = (TTC − PR_lot) / (1+t) × t` ; `HT = TTC − TVA`.
- Sans TVA : `TVA = 0` ; `HT = TTC`.

Identiques au moteur #1143 (`calculerTvaLot`) pour marge et totale,
sauf que le PR y est **calculé** (achat + ODF + notaire) / m² cessible
hors agricole, alors qu’ici le PR est **saisi** (décision Robin).

**Exemple Robin** (taux 20 % = 0,20) :

1a constructible, TVA totale, 90 000 € TTC :
TVA = 90 000 × 0,20 / 1,20 = **15 000** ; HT = **75 000**.
**Colle** à l’exemple (75 000 HT). Aucun PR nécessaire.

1b non constructible, TVA sur marge, 9 000 € TTC :
impossible sans PR saisi. **Illustration** si PR programme = 30 000 €
et surface_ref = 90 m² (seulement 1a, 1b exclu) **et** on applique
quand même 10 m² au numérateur (option incohérente, cf. C) :
PR_1b = 30 000 × 10 / 90 = 3 333,33 ;
TVA = (9 000 − 3 333,33) / 1,20 × 0,20 = 944,44 ; HT = 8 055,56.
Si PR_1b = 0 (exclu numérateur et dénominateur) :
TVA = 9 000 / 1,20 × 0,20 = 1 500 ; HT = 7 500.

Parent 99 000 = somme TTC enfants : OK dans l’exemple. La TVA parent
n’a **pas** de régime unique (hybride) : elle n’est que la somme des
TVA enfants, pas un 5ᵉ calcul.

**PR manquant** : proposer **non calculé + bandeau** (« Saisir le PR
foncier dans Financier → TVA sur marge »). Interdire un 0 qui ferait
croire une TVA totale. Aligné #1143 (`prixRevientFoncierM2` null) et
règle « pas de catch qui renvoie des zéros ».

**Figeage après acte** :
- Statut `acte` + `date_acte` existent.
- `prix_vente_ttc` déjà figé **à la réservation** (pas à l’acte).
- **Aucune** colonne TVA/HT/PR snapshot sur le lot.
- AdF : montants TTC/HT/TVA copiés à la génération (`/ 1.20` en dur).

Proposition (non codée) : à la transition `reserve → acte`, persister
`tva_acte_ht`, `tva_acte_tva`, `tva_acte_pr` (ou JSON) sur le lot
**feuille**. Un changement ultérieur du PR programme ne les touche
pas. Les lots non actés se recalculent.

### E. Références cadastrales (à terme, hors lot)

`parcelles` : `section_cadastrale`, `numero`, `code_insee_commune`,
surfaces, `constructible`. Lien lot commercial ⇄ parcelle : **aucun**
(grep `lot_commercial` × `parcelle` vide). `scenario_parcelle` relie
scénario ↔ parcelle, pas la grille de vente.

À prévoir plus tard : table pivot sous-lot ↔ parcelle (N:N probable :
un sous-lot peut recouvrir une fraction). Ne pas coller `parcelle_id`
trop tôt. Hors périmètre.

### F. Page TVA SUR MARGE

Aucun champ `pr_foncier` / `frais_notaire` sur `programmes` (dump +
modèle). Ne pas lire le bilan (`montant_etude_fi_*`).

**Insertion** : 3ᵉ sous-onglet FINANCIER (#1622) :
BILAN `#bilan` | BUDGET `#budget` (GS_ADMIN_ERP) | TVA SUR MARGE
`#tva-marge` (à créer). Routes `/bilan` et `/budget` conservées ;
nouvelle route dédiée (payload distinct, comme Budget).

**Tranches** (Robin réserve) : `scenario_lots.tranche` = typologie
Excel T1–T8, **pas** une tranche de commercialisation programme.
Pour ne pas bloquer : table du type
`programme_tva_marge (programme_id, tranche_id NULL, pr_foncier,
frais_notaire)` avec `tranche_id` NULL = montant programme. Quand les
tranches existeront, des lignes par tranche sans casser le niveau
programme. **Ne pas** créer la table maintenant.

### Découpage proposé (après arbitrages)

| Lot | Sujet | Risque |
|-----|--------|--------|
| A | Arbitrages Robin (ce diagnostic) | — |
| B | Page TVA SUR MARGE (PR + notaire, sous-onglet FINANCIER) | Moyen : `FinancierSubNav` / AppShell / Vite **série #1622** |
| C | Modèle régime (mode + taux) + type zone + dénominateur 0 | Élevé : ENUM `regime_tva` existant, AdF `/1.20` |
| D | Sous-lots (parent/enfants) + totaux feuilles | Élevé : `StockSyntheseCalculator`, résa, pivot, L’AURORE 138 m² |
| E | Figeage TVA à l’acte + correction AdF HT | Élevé : chaîne Sage / AdF |
| F | Cadastre sous-lots | Plus tard, Foncier |

### Collisions lots en cours

- **#1622** (poussé) : FINANCIER. Tout sous-onglet TVA touche
  `FinancierSubNav.vue` + `AppShell.vue` → rebase + rebuild série.
- **#1621** acompte Sage : autre zone, **même manifest Vite**.
- **#1625** avis d’imposition : poussé, pas de collision.
- Grille : `LotsGrillePanel.vue` / `Commercialisation.vue` — lots C–D.

### Points restant à arbitrer par Robin

1. Option parent **calculé** vs parent **saisi avec écart** (L’AURORE 138 m²).
2. Objet vendu : parent, enfants, ou les deux ?
3. Totaux : feuilles uniquement (recommandé si option 1) ?
4. Type de zone : 2 valeurs ou plus ?
5. Non constructible **dans** le dénominateur ou non, alors que 1b est
   en TVA sur marge (tension C).
6. Surface de la formule : `surface_terrain` (colonne Surface) — confirmer
   (pas SDP/CES).
7. Mode TVA vs ENUM taux actuel : remplacer ou ajouter une colonne ?
8. Taux 10 % social sur la grille commerciale : oui/non, porté par le régime ?
9. PR manquant : bandeau + non calculé (proposé) — confirmer.
10. Figeage à l’**acte** ou dès la **réservation** (comme le prix vente) ?
11. Qui voit TVA SUR MARGE (groupes) ? BUDGET est déjà admin-only.
12. ODF grille = `mode_vente=odf` : confirmer que c’est bien **ça** à
    exclure du dénominateur.

## DÉPLOIEMENT

```
git pull origin main
```

Pas de migrate, pas de journal, pas de npm. Documentation seule.

## LEÇON

La grille commerciale code un **taux** dans `regime_tva` ; le métier
Robin et la faisabilité parlent d’un **mode** (marge / totale / sans).
Les fusionner sans colonne dédiée cassera les 4 valeurs actuelles et
les AdF (`/ 1.20`). Parent SQL inexistant : le double comptage des
totaux est le premier piège. Le PR saisi (page FINANCIER) n’est pas
le PR du moteur Excel : ne pas brancher le bilan.
