---
suivi: 1573
date: 2026-09-16
sujet: Multiparcelle — recopie fantôme, lignes vides, messages FR, société fiche
chantier: foncier / parcelles
type: fix
statut: poussé
hash: 97bf9c52
fichiers:
  - resources/js/Components/Foncier/Prospection/ProspectionSaisieDialog.vue
  - resources/js/Pages/Foncier/Parcelles/Show.vue
  - app/Http/Controllers/Foncier/ParcelleController.php
  - app/Services/Foncier/ParcelleCreationService.php
  - tests/Unit/Parcelle1573SourceTest.php
  - tests/Unit/DetentionGroupe1532SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1573_fix_multiparcelle_et_societe_detentrice.md
---

# SUIVI #1573 — Fix multiparcelle et société détentrice

## PROMPT ENVOYÉ

Quatre défauts après #1569 (clos, comme #1567 / #1568 / #1572).
1. Recopie fantôme commune + société après Enregistrer.
2. Lignes vides en erreur ; pas de suppression de ligne.
3. Message Laravel en anglais ; placeholders A / 80 trompeurs.
4. Société détentrice : saisissable sans acte, verrouillée avec acte,
   abort_unless serveur, recalcul statut.

## FICHES LUES

- **#1572 LEÇON** — zone cliquable, pas de « vente » pour un acte
  d’acquisition. Impact : ne pas retraiter titre / destination / pastilles.
- **#1509 LEÇON** — 10 lignes vides intentionnelles ; grille minmax.
  Impact : conserver les 10 lignes ; la recopie n’était pas dans cette
  leçon, elle venait du padding post-erreur.
- **#1532 LEÇON** — détention = champ parcelle ; l’acte confirme et
  verrouille ; tester via `m365GroupNames()` brut. Impact : point 4
  applique cette règle (saisie manuelle jusqu’à l’acte).
- **#1569 LEÇON** — statut déduit, `recalculerIds`, pas de Cédée.
  Impact : brancher le recalcul sur le PATCH société, ne pas toucher
  au calcul.

## CAUSE ÉTABLIE DE LA RECOPIE FANTÔME

Pas un partage de référence à l’ouverture (`emptyRow()` est appelé
N fois). Pas un report serveur : `createBatchPerLine` ignore déjà les
lignes vides et ne recopie rien.

Cause : après une réponse avec erreurs, `ProspectionSaisieDialog.vue`
ne conservait que les lignes en erreur, puis **remplissait jusqu’à 3
lignes en recopiant commune (`code_insee_commune` + `_communeOpt`) et
société (`societe_detentrice_id`) depuis la dernière ligne en erreur**.
Scénario type : ligne 1 incomplète (section vide, placeholder « A »
pris pour une valeur) → erreur ; lignes 2–3, jusqu’alors vides,
réaffichent Clapiers et MAS CANTEROUX.

Même mécanisme dans `addRow()` (reprise de la ligne précédente) : retiré.

Conséquence métier : au second Enregistrer, les lignes 2–3 n’étaient
plus « vides » (commune + société postées) → validation partielle, ou
création avec société fantôme si l’utilisateur complétait section/numéro.
Au premier Enregistrer, si la ligne 1 était invalide, **aucune parcelle
n’était créée** ; la recopie était d’abord un défaut d’affichage, puis
un piège à la soumission suivante.

Correction : plus aucune recopie ; après erreur, conserver les lignes
non créées telles quelles ; `:key="row._uid"` pour ne pas réutiliser
un `CommuneSelect`.

## MESSAGES TRADUITS (modale / createBatch)

Préfixe écran : `Ligne N : ` + motif.

| Clé Laravel | Motif FR |
|---|---|
| section_cadastrale.required | la section est obligatoire. |
| section_cadastrale.string | la section est invalide. |
| section_cadastrale.max | la section ne peut pas dépasser :max caractères. |
| numero.required | le numéro est obligatoire. |
| numero.string | le numéro est invalide. |
| numero.max | le numéro ne peut pas dépasser :max caractères. |
| code_insee_commune.required | la commune est obligatoire. |
| code_insee_commune.string / .size | la commune est invalide. |
| code_insee_commune.exists | cette commune est inconnue. |
| surface_cadastrale_m2.numeric | la surface cadastrale doit être un nombre. |
| surface_cadastrale_m2.min | la surface cadastrale ne peut pas être négative. |
| destination.string | la destination est invalide. |
| destination.in | la destination n’est pas dans le référentiel. |
| zonage_plu.max | le zonage PLU ne peut pas dépasser :max caractères. |
| lieu_dit.max | le lieu-dit ne peut pas dépasser :max caractères. |

Déjà FR (inchangés) : « Ligne invalide. », doublon dans la saisie,
parcelle existante / corbeille, contrainte base, société détentrice
invalide (`normalizeSocieteDetentriceId`).

## POINT DE RECALCUL DU STATUT

`ParcelleController::updateChamp` : après `save()` de
`societe_detentrice_id`, `ParcelleStatutService::recalculerIds`.
Déjà présent (#1569) ; conservé.

`ActeAcquisitionService::creer` : `appliquerDetentionGroupe` fait un
`forceFill(['societe_detentrice_id' => $apres])` (l’acte écrase la
saisie manuelle), puis `recalculerIds`. Confirmé, non modifié.

PATCH avec au moins un acte : `abort_unless(..., 403)` — pas un Gate
(donc pas court-circuité par `Gate::before` / GS_ADMIN_ERP). Droit
d’écrire la détention : `DetentionGroupeSaisie::userPeutSaisir` lit
`m365GroupNames()` brut (`GS_HECTARE`, `GS_DSI`, `GS_ADMIN_ERP`).

## MAPPING SOCIÉTÉ DÉTENTRICE (fiche)

| Libellé affiché | Valeur postée | Colonne DB | Libellé back |
|---|---|---|---|
| Non renseignée | `null` (vide) | `parcelles.societe_detentrice_id` | Société détentrice |
| {nom} — {siret} | id entier `entites.id` | `parcelles.societe_detentrice_id` | Société détentrice |

## SYNTHÈSE

Recopie post-erreur supprimée. Lignes vides ignorées. Croix de
suppression. Messages FR. Société modifiable sans acte.

## DÉPLOIEMENT-TEST

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

Pas de migrate.

## LEÇON

Un padding « utile » après erreur (reprendre la commune de la ligne
valide) est une recopie métier : depuis #1569 la société produit
Détenue. Ne jamais recopier une ligne sur une autre. Un 422 JSON n’est
pas un `abort_unless` 403 : GS_ADMIN_ERP passe le Gate, pas
`abort_unless`. Placeholder sans « ex. » est lu comme une valeur.
