---
suivi: 1525
date: 2026-09-11
sujet: Vue lecture seule « Voir la saisie » + JSON ocr_raw.saisie
chantier: depot-factures / fiche-facture / consultation
type: feature
statut: poussé
hash: 5e58eaee
fichiers:
  - app/Support/DepotSaisieOcrJsonAcces.php
  - app/Http/Controllers/DepotFactureController.php
  - app/Http/Controllers/FactureController.php
  - routes/web.php
  - resources/js/Pages/DepotFactures/DepotFactureSaisieForm.vue
  - resources/js/Pages/DepotFactures/SaisieConsultation.vue
  - resources/js/Pages/Factures/Show.vue
  - resources/js/Components/DepotFactures/DepotSaisieOcrJsonButton.vue
  - tests/Unit/DepotSaisieConsultation1525SourceTest.php
  - tests/Feature/FactureFicheRouteTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1525_voir_la_saisie.md
---

# SUIVI #1525 — vue « Voir la saisie » en lecture seule

## PROMPT ENVOYÉ

Étape 0 : ouvrir Modifier puis enregistrer sans rien changer écrase-t-il
des champs à vide (hydratation colonnes vs ocr_raw.saisie, watcher
retenues, payload complet vs partiel, champ absent = conservé ou null) ?
Si OUI : ne pas coder, documenter. Si NON : bouton « Voir la saisie »
(mêmes cadres, tout grisé, aucun bouton d’action, aucune route
d’écriture), JSON brut GS_ADMIN_ERP / GS_DSI via m365GroupNames() brut,
visibilité = celle de la fiche. Pas de migration. Ne pas transformer
l’écran de saisie existant.

## BASE GIT

Rebasé sur `origin/main` à jour avant build Vite (`DepotFactureSaisieForm.vue`
zone commune).

## FICHES LUES

- **#1521 LEÇON** — watcher #1224 remet les % à 0 si % marché absent ;
  hors ERP = early-return. Impact : le watcher n’a pas `immediate` ;
  `marcheRetenuesPrefillEnabled` reste false jusqu’après hydratation.
- **#1224 LEÇON** — zéro RG/CP/OPC si % marché = 0, dépôt encore
  modifiable. Impact : normaliseur serveur à l’enregistrement (pas à
  l’ouverture) ; lecture seule `depotRetenuesModifiables` = false.
- **#1524 LEÇON** — Enregistrer post-engagement = PATCH compta, même
  `applyDepotSaisieFromValidated`. Impact : même contrat d’écriture.
- **#1450** — valideur JSON-only. Non touché.
- **#1276** — `readonly` Index = `can_edit === false`. Conservé.

Aucune fiche n’imposait une vue consultation distincte du Show éditable.

## ÉTAPE 0 — DIAGNOSTIC (conclusion : NON)

Ouvrir « Modifier » puis Enregistrer sans rien changer **n’écrase pas**
les retenues ni les champs du formulaire hydratés depuis `ocr_raw.saisie`.

Preuves :

1. **Source d’hydratation** — mélange colonnes dépôt (prioritaires pour
   FK / montants / dates) et `ocr_raw.saisie`. Les retenues
   (`retenue_garantie_pct`, `compte_prorata_pct`, `retenue_opc_pct`,
   `retenue_finitions_montant`, `montant_cie`) n’existent **pas** en
   colonnes : `useForm` L568-595 et `hydrateBrouillonDepuisDepot` L6878-6897
   lisent uniquement `ocr_raw.saisie`.
2. **Watchers zéro** — `peutSaisirRetenuesMarche` L4996 et #1224 L5013
   **sans** `immediate`. Ils ne tournent pas au montage. Le préremplissage
   marché (`applyRetenuesPrefillDepuisMarche`) est gated par
   `marcheRetenuesPrefillEnabled` (false jusqu’au nextTick post-hydrate,
   puis seulement sur changement utilisateur). `watch(showRG)` L5204 sans
   immediate : `showRG` part à false sans zéroer. `depotRetenuesModifiables`
   est faux si `readonly` ou statut figé → #1224 no-op.
3. **Payload** — `buildDraftPayload(form.data())` envoie **tous** les
   champs du formulaire (pas seulement les dirty). `0 ?? null` reste `0`.
4. **Champ absent** — dans `applyDepotSaisieFromValidated` L1721-1725,
   `retenue_* => $validated[...] ?? null` : un champ **absent** de la
   requête devient null. Le form les envoie toujours, donc ce chemin
   n’est pas pris sur Enregistrer normal.

Résidus (hors soupçon hydratation, non bloquants pour cette vue) :

- `$ocrRaw['saisie'] = $saisie` **remplace** l’objet : clés hors mapping
  (ex. `contexte_programme_id_verrou`) perdues à l’enregistrement.
- Normaliseur #1224 serveur : si le marché a désormais RG%=0 et le dépôt
  est encore modifiable, l’enregistrement force 0 (intention #1224).
- Destination FRAIS_GENERAUX : watcher `destination` `{ immediate: true }`
  remet les retenues à 0 à l’**ouverture** (L5326-5336) — hors PROGRAMMES.

## SYNTHÈSE

Page dédiée (pas modale) : le formulaire + PDF en splitter (~viewport)
dépasse largement une modale sans scroll excessif.

- GET `/depot-factures/{uuid}/saisie` — même visibilité que le dépôt
  (`findDepotForView` / `FactureVisibility`). Payload forcé
  `can_edit=false`, `lecture_seule_integrale=true`, pas de workflow
  d’écriture.
- GET `/depot-factures/{uuid}/saisie-ocr-json` — `ocr_raw.saisie` seul ;
  `DepotSaisieOcrJsonAcces` lit `m365GroupNames()` brut
  (GS_ADMIN_ERP | GS_DSI). Jamais Gate / hasAnyM365Group.
- Fiche facture : « Voir la saisie » dès qu’un dépôt UUID existe (plus
  limité aux comptabilisées non éditables). « JSON brut » si URL serveur
  fournie (groupes ci-dessus).
- Formulaire existant : prop `lectureSeuleIntegrale` défaut **false**.
  Aucun changement du chemin d’édition. En consultation : pas de
  préremplissage, pas de watchers zéro, champs retenues visibles même à 0.

### Mapping UI ↔ back (composants de choix)

Libellé affiché | Valeur postée | Colonne / JSON | Libellé back
--- | --- | --- | ---
Voir la saisie | GET `depot-factures.saisie` | — (lecture) | consultation saisie
JSON brut | GET `depot-factures.saisie-ocr-json` | `ocr_raw.saisie` | JSON saisie
(aucun POST/PATCH/DELETE sur ces routes)

## 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 migration.

Validation manuelle prod :

1. « Voir la saisie » sur un dépôt historique → tous les champs visibles,
   tous grisés, aucun bouton Enregistrer / Envoyer / Supprimer.
2. Champ à 0 en base (`retenue_garantie_pct`) → affiché à 0, pas masqué.
3. Compte non-admin → bouton JSON brut absent ; GET
   `/depot-factures/{uuid}/saisie-ocr-json` → 403.
4. Écran de saisie normal (Modifier) → inchangé.

## LEÇON

Les retenues vivent dans `ocr_raw.saisie`, pas en colonnes. Un Enregistrer
à vide les conserve parce que le form poste tout le payload. En revanche
`applyDepotSaisieFromValidated` **remplace** l’objet `saisie` : une clé
hors mapping disparaît. Pour inspecter l’état réel sans ce risque, une
route GET dédiée avec `can_edit` forcé false est plus sûre que réutiliser
`depot-factures.show`. JSON admin : `m365GroupNames()` brut, jamais Gate.
