---
suivi: 1275
date: 2026-08-11
sujet: Diag droits modification dépôt / CIE grisé NV1
chantier: depot-factures
type: diagnostic
statut: poussé
hash: 28c25034
fichiers:
  - tools/diag/diag_droits_modification_depot_1275.php
  - docs/suivi/SUIVI_1275_diag_droits_modification_depot.md
---

# SUIVI #1275 — Diagnostic droits de modification dépôt (PHASE 1)

## PROMPT ENVOYÉ

Diagnostic strict (pas de code applicatif). Règles cibles R1–R4 Robin. Défaut prod dépôt 1230 : NV1 peut modifier RG/CP/finitions/pénalités, case CIE grisée et muette. Fix = #1276 après arbitrage.

## R1–R4 (acquis, non rediscutés)

| # | Règle |
|---|---|
| R1 | Quiconque peut **valider NV1** peut **tout modifier** avant sa validation |
| R2 | Idem NV2 |
| R3 | `GS_ADMIN_ERP` peut modifier **quel que soit le stade** |
| R4 | `GS_COMPTABILITE` peut modifier **à l’étape comptabilisation** |
| « Tout » | montants, retenues (RG, CP, OPC, CIE, finitions, pénalités), rattachement, dates |

## OBJET 1 — Deux notions de « modifiable »

### a) `canEditDepot` (verbatim `DepotFactureController` L3984–4070)

```php
public function canEditDepot(DepotFacture $depot): bool
{
    $user = auth()->user();
    if (! $user instanceof User) {
        return false;
    }

    if ((string) $depot->statut === DepotFacture::STATUT_ARCHIVE) {
        return false;
    }

    $depot->loadMissing('facture');
    if (FactureStatutService::estComptabiliseeOuAuDela($depot)) {
        // GS_ADMIN_ERP seul peut éditer post-comptabilisation (#658) — pas GS_DSI.
        if (! FactureAdminErpEditOverride::userIsAdminErp($user)) {
            return false;
        }

        return true;
    }

    if ($user->isAdminERP()) {
        return true;
    }

    $groupes = $user->groupes_m365 ?? [];
    if (! is_array($groupes)) {
        $groupes = [];
    }
    $statut = (string) $depot->statut;

    if ($statut === DepotFacture::STATUT_A_COMPTABILISER
        && in_array('GS_DSI', $groupes, true)) {
        return true;
    }

    // Groupes toujours autorisés à modifier (tous statuts)
    $groupesAdmin = ['GS_COMPTABILITE', 'GS_ADMIN_ERP'];
    if (array_intersect($groupesAdmin, $groupes) !== []) {
        return true;
    }

    // GS_DIRECTION_GENERALE peut modifier jusqu'à validation NV2
    if (in_array('GS_DIRECTION_GENERALE', $groupes, true)) {
        return true;
    }

    // Statut en_validation_nv2 → NV1 bloqué, seulement NV2+
    if ($statut === 'en_validation_nv2') {
        // Seul le validateur NV2 (PDG) peut modifier.
        // À adapter si une identification explicite du PDG est ajoutée.
        return false;
    }

    // Statut en_validation_nv1 : déposant + validateur NV1 peuvent modifier
    if ($statut === 'en_validation_nv1') {
        if ((int) $depot->user_id === (int) $user->id) {
            return true;
        }
        if ($depot->validateur_email === $user->email) {
            return true;
        }

        return false;
    }

    // Statuts initiaux … → déposant ou validateur désigné
    if (in_array($statut, [
        DepotFacture::STATUT_DEPOSE,
        DepotFacture::STATUT_ATTRIBUE,
        DepotFacture::STATUT_ANALYSE,
        DepotFacture::STATUT_EN_ANALYSE,
        DepotFacture::STATUT_EN_COURS_SAISIE,
    ], true)) {
        if ((int) $depot->user_id === (int) $user->id) {
            return true;
        }
        if ($this->utilisateurEstValidateurDesigneDepot($user, $depot)) {
            return true;
        }
        if (in_array('GS_DSI', $groupes, true)) {
            return true;
        }
    }

    return false;
}
```

Appels liés (lecture) : `utilisateurEstValidateurDesigneDepot` → `utilisateurEstAttributaire` → `FactureValidateurNv1AssignmentService::resolveUser`.  
`resolveWorkflowModificationMeta` : `can_enregistrer` = `canEditDepot` ; `can_validate` = `FactureValidationPermissions::permissionsForPendingStep`.  
`validerWorkflowEngage` : gate sur `can_validate`, **pas** sur `canEditDepot` pour l’acte de valider.

### b) `depotRetenuesModifiables` (verbatim, entier)

```js
const depotRetenuesModifiables = computed(() => {
    if (props.readonly) {
        return false;
    }
    const statut = String(props.depot?.statut ?? '');
    if (['rejete', 'archive', 'valide', 'paye', 'comptabilisee'].includes(statut)) {
        return false;
    }
    if (props.depot?.can_edit === true) {
        return true;
    }
    if (workflowModification.value?.can_enregistrer === true) {
        return true;
    }
    if (!workflowDejaEngage.value) {
        return true;
    }

    return false;
});
```

### c) Statuts

**Dépôt** (`DepotFacture::statutsAutorisesColonne`) : `depose`, `attribue`, `en_analyse`, `analyse`, `en_cours_saisie`, `en_validation_nv1`, `en_validation_nv2`, `a_comptabiliser`, `comptabilisee`, `valide`, `paye`, `rejete`, `archive`.  
**Facture** : `recue`, `en_validation_nv1`, `en_validation_nv2`, `a_comptabiliser`, `comptabilisee`, `en_paiement`, `payee`, `rejetee`.  
Comptes prod : **script §2** (à coller après exécution OVH).  
**R4** : étape = `a_comptabiliser` (file compta). `comptabilisee` (+ paiement) = `estComptabiliseeOuAuDela` → hors R4, override AdminERP (#658).

### d) Divergence `canEditDepot` ↔ `depotRetenuesModifiables`

Sur Index : `readonly: can_edit === false`. Donc **si `can_edit` true → `readonly` false → `depotRetenuesModifiables` true** (statut hors liste figée).  
Divergence réelle plutôt **`canValidate*` ≠ `canEditDepot`** (voir OBJET 3), et **CIE** via `cieCaseActivatable` / dues API, pas seulement `readonly`.

## CORRECTION de la cause tracée (important)

Le prompt attribuait le non-chargement des dues à L4510 + `depotRetenuesModifiables`. **Faux** :

- L4510 = watch de **normalisation** RG/CP/OPC (mise à 0 si % marché N/A).
- `refreshCieDues()` est déclenché par `watch([marche_id, fournisseur_id, uuid], …, { immediate: true })` — **sans** gate `depotRetenuesModifiables`.
- Case CIE : `:disabled="readonly || isCieHistorique || !cieCaseActivatable"`.
- `cieCaseActivatable` exige dues / montant / lignes ; silence motif si `cieDuesLoading` ou motif historique vide.

Hypothèses restantes pour 1230 (à confirmer par le script prod) : dues OK mais UI ; **403** `authorizeCieRead` ; mismatch fournisseur/marché ; ou autre.

## OBJET 2 — Consommateurs `canEditDepot`

| fichier:ligne | Protège | Élargissement autoriserait | Risque |
|---|---|---|---|
| `DepotFactureController:575` | `associerCie` écriture association | Associer CIE hors stade | **écriture base** — à valider |
| `DepotFactureController:1666` | `update` saisie | PATCH montants/retenues/rattachement/ventilations | **écriture** + sync CIE prélèvements — **à valider** |
| `DepotFactureController:3674/3676` | payload `can_edit` / consultation | Affichage seul | aucun |
| `DepotFactureController:3790/3792` | idem page Show/data | Affichage | aucun |
| `DepotFactureController:3900` | `can_enregistrer` workflow meta | Bouton Enregistrer | aligné update |
| `DepotFactureController:4483` | `depotRetenuesPeuventEtreNormalisees` | Soft-zero retenues serveur | écriture via normalize — faible |
| `DepotFactureController:4586` | `assertCieRetenueServeur` | Poster CIE si stade « non modifiable » | **écriture prélèvements** — à valider |
| `DepotFactureController:4701` | JSON `modifiable` dues CIE | Signal UI | aucun |
| `ComptabilisationController:222/237` | `update` compta | Modifier saisie en file compta | **écriture** + lignes — **à valider** (R4) |
| `ComptabilisationController:667` | `comptabiliser` clôture | Passer statut compta | **écriture statut** — à valider |
| `ComptabilisationController:745` | `executerComptabilisationApbill` | **Envoi Sage APBILL** | **SAGE** — **à valider** |
| `FactureController:717` | `can_edit_depot` fiche | Lien édition | dérivé |

### Front — `depotRetenuesModifiables` vs `readonly`

| Dépend de | Champs |
|---|---|
| `readonly` seul (surtout) | Finitions, pénalités, dates, rattachement (fieldset), OPC/RG/CP cases (sauf historique) |
| `depotRetenuesModifiables` | `cieCaseActivatable` / motif ; `isRg/Cp/Opc/CieHistorique` ; pré-coche dues ; watch zéro RG/CP/OPC |
| `cieCaseActivatable` (composée) | Case CIE + UI lignes dues |

## OBJET 3 — « Pouvoir valider NV1/NV2 »

Source de vérité : **`FactureValidationPermissions`** (`canValidateNiveau`, `canValidateNv1`, `canValidateNv2`, `permissionsForPendingStep`).

- NV1 : `validateur_id` pending **ou** `DepotFactureValidateurEffectif` / `resolveValidateurDepuisSaisie` **ou** DG **ou** `ProgrammeResponsable` (conducteur/responsable) **ou** GS_COMPTABILITE si frais généraux.
- NV2 : DG **ou** `validateur_id` pending nv2.
- DSI / AdminERP : toujours via `hasAnyM365Group` (court-circuit Admin).

Colonnes : `workflow_validations.validateur_id` + `ocr_raw.saisie.validateur_id/email` + `depot_factures.validateur_email`.

**Écart critique** : `canEditDepot` en `en_validation_nv1` n’utilise que `user_id` déposant + **`validateur_email === email` (strict)** — **pas** `utilisateurEstValidateurDesigneDepot` ni les responsables programme. En `en_validation_nv2` : **`return false`** pour tout non déjà passé par GS_COMPTA/DG/Admin/DSI — le validateur NV2 désigné **ne peut pas éditer** via cette branche.

`factures.validation_nv1` / `_nv2` : catalogue + `dashboard_widgets` — **0 usage applicatif**. **#1276 : réutiliser `FactureValidationPermissions`, ne pas « brancher » les permissions mortes** sans lot droits dédié.

## OBJET 4 — Court-circuit `GS_ADMIN_ERP`

- `User::hasAnyM365Group` / `hasM365Group` : si `isAdminERP()` → **true d’emblée**.
- `Gate::before` (`AppServiceProvider:60`) : AdminERP → autorise toute ability.
- **R3 déjà satisfaite** pour édition post-compta (`FactureAdminErpEditOverride`) et `isAdminERP()` précoce dans `canEditDepot`. **#1276 : ne pas ajouter de code redondant AdminERP.**
- Test `hasAnyM365Group(['GS_ENVOL','GS_DSI','GS_ADMIN_ERP'])` pour Lahouari : **script §4** doit lister ses groupes **réels** — si `isAdminERP()` false et true quand même → vrai membre ; si `isAdminERP()` true → court-circuit.
- Permissions décochées `/admin/droits` pour AdminERP : **sans effet** tant que Gate::before / hasAnyM365Group existent (hors périmètre).

## OBJET 5 — Matrice actuel vs R1–R4

Légende case : `auj.` / `R*` — **CHANGEMENT** si diffèrent.

Profils : S=saisisseur (déposant), V1=validateur NV1 désigné, V2=NV2 désigné, C=`GS_COMPTABILITE`, A=`GS_ADMIN_ERP`, L=autre.

| Statut dépôt | S | V1 | V2 | C | A | L |
|---|---|---|---|---|---|---|
| depose…en_cours_saisie | auj oui / R oui | auj oui* / R oui | auj non† / R non | auj oui / R oui | auj oui / R oui | auj non / R non |
| en_validation_nv1 | auj oui / R oui | auj oui‡ / **R oui** | auj non / R non | auj oui / R oui | auj oui / R oui | auj non / R non |
| en_validation_nv2 | auj non§ / R non | auj non / R non | auj **non** / **R oui** **CHANGE** | auj oui / R oui | auj oui / R oui | auj non / R non |
| a_comptabiliser | auj non§ / R non | auj non / R non | auj non / R non | auj oui / **R oui** | auj oui / R oui | auj non / R non |
| comptabilisee+ | auj non / R non | auj non / R non | auj non / R non | auj non / R non | auj oui / R oui | auj non / R non |
| archive/rejete | non / non | non / non | non / non | non¶ / non | non¶ / non | non / non |

\* via `utilisateurEstValidateurDesigneDepot` sur brouillon.  
‡ via email colonne **strict** — plus étroit que `canValidateNv1` → **CHANGE possible** si V1 valide mais email colonne ≠.  
† V2 non concerné en brouillon.  
§ sauf si C/DG/Admin déjà true.  
¶ archive : `canEditDepot` false même Admin ; rejet : C/Admin true aujourd’hui (intersect groupes) — **à arbitrer**.

**Élargissements R1/R2 non explicitement listés par Robin mais impliqués** : rattachement (programme/marché/fournisseur), ventilations, dates — car `update` / `applyDepotSaisieFromValidated` sont **globalement** gated par `canEditDepot`, pas champ par champ.

Matrice **calculée** sur échantillons : script §6 (1 dépôt/statut).

## OBJET 6 — Arbitrages (non tranchés)

a) **NV2 modifie après NV1** : `applyDepotModificationEngageeDepuisRequete` **ne repartit pas** en NV1 ; historique `modifie` seulement. R2 « tout modifier » n’implique pas à elle seule le reset — **à arbitrer**.  
b) **`logHistoriqueIfSaisieChanged`** : diff `FactureHistoriqueSaisie` avant/après + `logHistorique` (user/timestamp via `Historisable`). Appelé : `update`, modification engagée, `ComptabilisationController::update`. Pas prouvé sur tous chemins Sage.  
c) **R4 / `lignes_comptables`** : figées à la comptabilisation (#1263). R4 doit s’arrêter **avant** `comptabilisee` / envoi Sage — typiquement `a_comptabiliser` uniquement.  
d) **Autres champs** liés à `depotRetenuesModifiables` : historiques RG/CP/OPC/CIE + watch zéro ; finitions/pénalités surtout `readonly`.

## OBJET 7 — Script

`tools/diag/diag_droits_modification_depot_1275.php` — lecture seule, bootstrap absolu, `display_errors=1`.  
Exécution locale 11/08/2026 : **MySQL 127.0.0.1 refusé** → sortie d’échec explicite (« RESULTAT 0 NE VALIDE RIEN »). **À rejouer sur OVH** et coller la sortie dans #1276.

## LEÇON

Ne pas élargir `canEditDepot` sans inventaire : il gate aussi **APBILL Sage** et sync CIE. Deux notions « modifiable » + CIE `cieCaseActivatable` ≠ simple `readonly`. Permissions catalogue NV1/NV2 mortes ≠ `FactureValidationPermissions`.

## Vérification

Lecture de code + script local sans DB. **Pas d’écran réel ERP.**

## DÉPLOIEMENT

Aucun code applicatif. Sur OVH (lecture) :

```bash
php -d display_errors=1 tools/diag/diag_droits_modification_depot_1275.php
```
