---
suivi: 1366
date: 2026-08-31
sujet: diag_verrou_nv1_comptable
chantier: comptabilisation
type: diagnostic
statut: poussé
hash: 12cb8ff0
fichiers:
  - docs/suivi/SUIVI_1366_diag_verrou_nv1_comptable.md
  - tools/diag/diag_verrou_nv1_comptable_1366.php
---

## PROMPT ENVOYÉ

PHASE 1 — DIAGNOSTIC. Verrou « Retenues et montants verrouillés : ce dépôt a franchi NV1 »
bloque l'enregistrement en comptabilisation (cas E-04 HECTARE, FG, F3927 SCI TECNOVER,
retenues 0 €). Règle métier Robin : édition comptable jusqu'à comptabilisation, pas NV1.
Ne pas toucher #1364 ni fix fournisseur #1365.

## FICHES LUES

- `#1301` — **origine du verrou** : assertRetenues 422 + fieldset front + buildDraftPayload.
- `#1363` — dirty orange à l'ouverture (baseline #1301) ; lien possible avec échec save.
- `#1364` — volet Comptabilité sibling ; **non touché**.
- `#1365` — pattern script diag SHOW COLUMNS + try/catch.

## SYNTHÈSE (6 objets)

### OBJET 1 — LA RÈGLE ACTUELLE

| Couche | Emplacement | Critère |
|--------|-------------|---------|
| **Serveur (422)** | `DepotFactureController::assertRetenuesEtMontantsVerrouillesPostNv1` (~4727) appelé depuis `applyDepotSaisieFromValidated` (~1473) | `FactureStatutService::estAuDelaDeNv1Inclus($depot)` |
| **Bypass** | `DepotRetenuesNv1Lock::userPeutModifierRetenuesEtMontants` | `GS_ADMIN_ERP` dans `m365GroupNames()` **brut** (pas `hasAnyM365Group` / `isAdminERP`) |
| **Compta** | `ComptabilisationController::update` (~282) → même `applyDepotSaisieFromValidated` | idem |
| **Front (UI)** | `DepotFactureSaisieForm.vue` : `depotFranchiNv1` (~3862), `retenuesEtMontantVerrouilles` (~3874), bandeau ~8731 | `depot.statut` ∈ `DEPOT_STATUTS_NV1_PLUS` (~3853) |

**Seuil statut (normalisé, pas seulement colonne dépôt)** :
`FactureStatutService::estAuDelaDeNv1Inclus` → vrai pour
`nv1_en_cours`, `nv2_en_cours`, `a_comptabiliser`, `comptabilisee`, `mise_en_paiement`,
`en_paiement`, `payee` (via `normalize()` : **factures.statut prioritaire** si `facture_id`).

Bruts ENUM `depot_factures.statut` (migration `2026_05_29_…`) :
depose, attribue, en_analyse, analyse, en_cours_saisie, en_validation_nv1,
en_validation_nv2, a_comptabiliser, comptabilisee, valide, paye, rejete, archive.

Pas d'horodatage ni flag dédié : **pipeline statut / facture**.

`canEditDepot` (~4087) : GS_COMPTABILITE **peut** éditer avant comptabilisation — le blocage
n'est pas un 403, c'est le **422 assert** sur champs verrouillés.

### OBJET 2 — PÉRIMÈTRE DU VERROU (POINT CENTRAL)

**Robin : blocage total enregistrement — CONFIRMÉ côté API, INFIRMÉ côté UI.**

| | Comportement |
|---|-------------|
| **UI** | Seul le `<fieldset :disabled="retenuesEtMontantVerrouilles">` (~8727) : cartes **Montant** + **Retenues**. Service, analytics, mode règlement, compte bancaire, établissement restent **éditables** (`comptabilisationMode`). |
| **PATCH** | `buildDraftPayload` (~6450) envoie **tout** le brouillon (montants, retenues, CIE, lignes_tva, penalite_ids **+** champs compta). |
| **Serveur** | Si **un seul** champ verrouillé du payload **diffère** de `ocr_raw.saisie` → `ValidationException` 422 → **aucune** persistance (y compris service/analytics). |

Champs vérifiés par l'assert (liste exhaustive ~4759-4860) :
`retenue_garantie_pct`, `compte_prorata_pct`, `retenue_finitions_montant`, `retenue_opc_pct`,
`montant_cie`, `cie_montant_ht`, `montant_ht`, `montant_tva`, `montant_ttc`,
`retenue_finitions_mode`, `deduire_cagnotte_cp`, `cie_lignes`, `penalite_ids`, `lignes_tva`.

**Non listés** (devraient passer si seuls modifiés) : `service_id`, `analytics_id`,
`mode_reglement`, `compte_bancaire_id`, `etablissement_id`, `lignes_comptables`, `libelle_intacct`, etc.

**Effet pratique** : blocage **total** dès qu'un champ verrouillé du payload diverge — typiquement
`cie_lignes`/`cie_montant_ht` (showCIE + buildCieLignesPayload), `penalite_ids`, arrondis TVA,
ou dirty baseline (#1301/#1363) sans saisie utilisateur.

### OBJET 3 — LE CAS SANS RETENUE

Le verrou ne teste **pas** `total_retenues`, la présence d'un marché, ni `sans_marche`.
Dès `estAuDelaDeNv1Inclus` → assert actif même si :
- retenues affichées = **0,00 €** (RG/CP/OPC/CIE/finitions/pénalités nulles en `ocr_raw.saisie`) ;
- `destination = FRAIS_GENERAUX` sans marché (cas E-04 / F3927 SCI TECNOVER).

**Anomalie confirmée (code)** : un FG sans retenue en file compta subit le **même** garde qu'un marché
avec RG validée en NV1. Le seuil ne tient pas compte du montant retenu.

Chiffres prod : script OVH **OBJET 4** (`COUNT NV1+ FG sans retenue`).

### OBJET 4 — AMPLEUR

Requêtes **OBJET 4** du script (lecture seule, `only_full_group_by` actif — pas de GROUP BY ambigu) :

| Mesure | Requête |
|--------|---------|
| Dépôts NV1+ (verrou actif approx.) | `statut_brut_effectif IN (en_validation_nv1 … payee)` avec `factures.statut` prioritaire |
| NV1+ avant comptabilisation | idem excl. comptabilisee / valide / paye / mise_en_paiement / en_paiement |
| NV1+ frais généraux | heuristique `destination=FRAIS_GENERAUX` ou marche_id NULL + categorie_id |
| NV1+ FG sans retenue | heuristique pct/montants CIE nuls en JSON saisie |

Coller la sortie brute OVH dans le ticket (DB locale Cursor indisponible).

### OBJET 5 — DEPUIS QUAND

| Commit | Date | SUIVI |
|--------|------|-------|
| `8f5a4a12afd056453b9c42d300643631d05ad447` | 2026-08-12 18:10:16 +0200 | **#1301** — assert 422 + `DepotRetenuesNv1Lock` + fieldset front |

Comptables GS_COMPTABILITE bloqués sur retenues/montants dès **12/08/2026** (déploiement #1301).
Robin (seul GS_ADMIN_ERP) ne voit pas le blocage (piège droits documenté #1301).

### OBJET 6 — POURQUOI CE VERROU EXISTE + OPTIONS (ne pas trancher)

**Intention #1301** : empêcher qu'un validateur NV1 approuve des montants/retenues, puis qu'un
tiers les modifie avant compta / Intacct. Légitime sur **montants validés**.

**Distinction Robin** : verrouiller montants/retenues ≠ verrouiller tout le formulaire compta.

| Option | Description | Risque à signaler |
|--------|-------------|-------------------|
| **A** | Déplacer le seuil NV1 → `comptabilisee` (`estComptabiliseeOuAuDela`) | Entre NV1 et compta, montants modifiables par compta — écart vs validation NV1 |
| **B** | Garder assert sur montants/retenues seulement ; PATCH partiel ou ignorer champs verrouillés inchangés côté serveur ; ne pas envoyer drift CIE/TVA depuis le front | Complexité ; deux sources de vérité si mal câblé |
| **C** | Combiner A + B : assert montants jusqu'à compta ; champs compta toujours patchables | Meilleur alignement métier ; impl + tests manuels GS_COMPTABILITE obligatoires |

**Lien #1363 / dirty (#1301 baseline)** : `captureOpeningBaselineIfPending` post-sync CIE (#1301)
aligne `isDraftDirty` sur le snapshot front, **pas** sur `ocr_raw.saisie`. Le bouton orange
« Enregistrer (modifications non sauvegardées) » à l'ouverture (#1363) coexiste avec un PATCH
qui envoie quand même `buildDraftPayload` complet : si `montant_ht`, `lignes_tva`, `cie_lignes`
ou `penalite_ids` divergent du JSON serveur (hydratation #849, `showCIE`/`buildCieLignesPayload`,
sync compta lignes), l'assert 422 se déclenche **sans** que le comptable ait touché aux retenues.
**Lien plausible — à confirmer** sur dépôt OVH (DevTools : payload PATCH vs `ocr_raw.saisie`).

Bandeau front (« retenues et montants en lecture seule ») vs toast serveur sur **Enregistrer**
global : UX incohérente avec OBJET 2 — le comptable édite service/analytics puis voit l'échec.

Test obligatoire fix : compte **GS_COMPTABILITE** (ex. Sabrina BARTOLI, Rudy BENOR), pas Robin Admin ERP.
Tests sqlite compta : **76 skipped** — test manuel réel obligatoire si fix `DepotFactureController` / `ComptabilisationController`.

## DÉPLOIEMENT-TEST (diag)

```
cd /home/hectare-envol/envol.hectare.fr
php -l tools/diag/diag_verrou_nv1_comptable_1366.php
php -d display_errors=1 tools/diag/diag_verrou_nv1_comptable_1366.php
php -d display_errors=1 tools/diag/diag_verrou_nv1_comptable_1366.php F3927
```

## LEÇON

Verrou métier sur **montants** + PATCH **monolithique** = blocage de facto de toute la compta.
Ne pas confondre `:disabled` fieldset (UI partielle) et assert sur payload complet (API totale).
Tester avec GS_COMPTABILITE ; `hasAnyM365Group` / Robin Admin masquent le symptôme.
