---
suivi: 1488
date: 2026-09-08
sujet: Diagnostic frise NV2 fiche 1370 (deja_reglee saute NV1/NV2)
chantier: factures
type: diagnostic
statut: poussé
hash: c6cabddb
fichiers:
  - docs/suivi/SUIVI_1488_diag_frise_nv2_1370.md
---

# SUIVI #1488 — PHASE 1 DIAGNOSTIC — fiche 1370 frise NV2 non actionnable

## PROMPT ENVOYÉ

PHASE 1 diagnostic seul. Fiche `/programmes/16/factures/1370` (dépôt
1735) : badge « Validation NV2 » + « Déjà réglée », frise Analyse seule
validée, NV1/NV2 chevrons gris, action Valider NV2 indisponible. Base
cohérente (WV nv1 valide, nv2 en_attente). Trois anomalies à ne pas
confondre. Aucun code applicatif, aucune reprise des dépôts 1634/1735.

## BASE GIT

Prompt : HEAD prod attendu `a6098cca` (#1485), #1487 annoncé en cours
sur ComptaTableauBord*. Réel : `8fb55b18` chore hash fiche #1487 —
origin/main à jour, #1487 déjà poussé. Pas de rebase. Aucun fichier
ComptaTableauBord* / PHP / Vue / migration modifié.

MySQL agent : 127.0.0.1:3306 refused. Aucun chiffre inventé.
Aucun PHP modifié.

## FICHES LUES

- **#1486** (LEÇON) — enseigne de circuit = `programmes.enseigne_id` ;
  `entites.enseigne_id` NULL n’est plus un signal NV2. Dépôts 1634 et
  1735 : reliquats à valider à la main, pas de script. Impact : confirmer
  `factureAvecNv2()` conserve un WV nv2 déjà ouvert ; ne pas traiter 1634
  (deja_reglee=0) comme 1370 (deja_reglee=1).
- **#1485** (LEÇON) — badge ≠ filtre ; `deja_reglee` visible sur l’onglet
  tous. Impact : le badge « Déjà réglée » sur la fiche n’est pas un
  filtre liste ; il alimente le *type* de frise.
- **#1484** (LEÇON, périmée par #1486) — à l’époque `entites.enseigne_id`
  décidait le **si** NV2. Plus le cas. Impact : reliquats RACINE attendus.

## VERDICT (en tête) — anomalie 1

**ÉTABLI.** La frise ne lit PAS `facture_historique`. Elle saute NV1 et
NV2 parce que `depot_factures.deja_reglee = 1` force le type `deja_reglee`,
qui met `nv1` et `nv2` dans `skippedSet`. Les chevrons gris sont l’icône
`skipped` (`pi-angle-double-right`). Le bouton Valider disparaît parce
que Vue exige une étape *courante* `nv1` ou `nv2` : or NV2 skipped n’est
jamais `current`. Les droits GS_ADMIN_ERP sont OK (`can_validate` true).

Ce n’est PAS un trou d’historique (anomalie 2). Ce n’est PAS un reliquat
d’enseigne seul (sinon 1315 serait bloquée aussi : `deja_reglee = 0`).

---

## Anomalie 1 — frise désalignée + action NV2 indisponible

### 1. Source exacte de chaque étape de la frise

Fichier unique du calcul : `app/Support/FactureFriseEtapePresenter.php`
`buildWorkflowComputation()` (L87–228). Branché par
`FactureController::buildWorkflowForDisplay()` L1615.

Source | Lit | Ne lit pas
`naturalSteps` | type facture + `FacturePasseParValidationNv2` + présence WV nv2 | historique
`type` | `depotFacture.deja_reglee` / `saisie_historique` / `deja_valide` (L31–49) | historique
`pending` | premier `workflow_validations.statut = en_attente` (L111) | historique
`displayStatut` | `FactureStatutService::normalize($facture)` = `factures.statut` (L112, service L173–177) | `depot_factures.statut` (pas sur cette branche Facture)
`stepStates` nv1/nv2 | `skippedSet` puis pending / displayStatut (L151–214) | historique
date Analyse | `factures.created_at` via `FactureController` L1654–1658 | historique
date NV1/NV2 | `WorkflowValidationStepPresenter::metaPourEtape` → WV | historique

Extrait type + skip (L41–69) :

    if ((bool) ($depot?->deja_reglee ?? false)) {
        return 'deja_reglee';
    }
    …
    'deja_reglee' => [...$validationSkipped, ...$etapesPaiement],
    // $validationSkipped = nv1+nv2 si la facture a (ou aurait) une NV2

Extrait pending annulé si skip (L135) :

    $effectivePendingKey = ($pendingKey !== null && isset($skippedSet[$pendingKey])) ? null : $pendingKey;

Le dépôt EST chargé sur la fiche : `FactureController::renderShowPage`
L663–673 sélectionne `depot_factures.deja_reglee`. Donc le type
`deja_reglee` est actif pour 1370.

### 2. Pourquoi NV1/NV2 « non franchies » alors que WV 4195/4196 existent

Hypothèse « la frise s’appuie sur facture_historique (valide_nv1 absent) »
: **INFIRMÉE.** Aucune lecture de `facture_historique` dans le presenter
ni dans `buildWorkflowForDisplay`.

Chaîne établie pour 1370 (`deja_reglee = 1`, WV nv2 présent) :

1. `resolveFactureType` → `deja_reglee`
2. `resolveSkippedWorkflowStepKeys` → `nv1`, `nv2`, plus les étapes paiement
3. Boucle L152–156 : `stepStates[nv1] = skipped`, `stepStates[nv2] = skipped`
4. `pendingKey = nv2` (WV 4196 en_attente) puis `effectivePendingKey = null`
5. `analyse` → `past` (L179 : `displayStatut !== 'analyse'`)
6. Aucune étape `current`

Rendu Vue `Show.vue` L1290–1292 + L1020 : `state === 'skipped'` → pastille
grise + icône `pi-angle-double-right` (double chevron). C’est exactement
l’observation Robin (« chevrons gris »). Une étape `future` afficherait
un numéro, pas un chevron ; une étape `past` afficherait ✓ vert.

Badge « Validation NV2 » : `display_statut` = `nv2_en_cours` (normalisation
de `factures.statut` ∈ en_validation_nv2 / en_attente_nv2 / nv2_en_cours)
via `statutBadgeConfig.js` (`nv2_en_cours` → « Validation NV2 »). Le badge
suit le **statut facture**, pas `stepStates`. D’où la contradiction badge
vs frise.

### 3. Condition de l’action « Valider » — ce qui échoue pour 1370

Pas un Gate. Pas `hasAnyM365Group` qui refuserait Robin.

Droits (OK pour GS_ADMIN_ERP) :

- `User::m365GroupNames()` L119–122 : retourne `m365_groups` brut.
- `User::isAdminERP()` L103–114 : `in_array('GS_ADMIN_ERP', $groupes)`.
- `User::hasAnyM365Group()` L146–149 : **si `isAdminERP()` → true d’emblée**
  (avant l’intersect).
- `Gate::before` `AppServiceProvider` L69–75 : `isAdminERP()` → true.
  N’intervient pas ici : la fiche n’appelle pas `$this->authorize()`.
- `FactureValidationPermissions::canValidateNiveau` L102–104 : GS_ADMIN_ERP
  → true.
- `permissionsForPendingStep` L132–139 : pending WV nv2 existe →
  `can_validate = true`.

Le serveur envoie donc `permissions.can_validate = true`.

Masquage Vue — `Show.vue` L602–616 :

    const actionableStepKeys = ['nv1', 'nv2'];
    const currentStepKey = steps.find(s => s.state === 'current')?.key
    showHeaderValidationActions =
      display_statut ∈ {en_validation_nv1, en_validation_nv2, nv1_en_cours, nv2_en_cours}
      && actionableStepKeys.includes(currentStepKey)
      && (can_validate || can_reject || can_block)

Pour 1370 : `display_statut` est un statut NV2 (badge le prouve) donc le
premier test passe. `currentStepKey` est **null** (aucune étape current).
`includes(null)` → false. Bouton L1698–1706 non rendu.

Condition qui échoue : **absence d’étape frise `current` nv1/nv2**, causée
par le skip `deja_reglee`. Pas les droits.

### 4. `deja_reglee` dans frise et action

Oui, aux deux endroits :

- Frise : `resolveFactureType` L41–43 → skip NV1/NV2 L65.
- Action : indirect — le skip empêche `current`, donc
  `showHeaderValidationActions` faux.

Le flag n’est pas relu dans `FactureValidationPermissions`.

### Point de code à modifier (PHASE 2 — pas de fix ici)

1. `FactureFriseEtapePresenter::resolveSkippedWorkflowStepKeys` — le type
   `deja_reglee` saute `nv1`/`nv2` même si un WV nv2 `en_attente` existe.
2. `Show.vue` `showHeaderValidationActions` — exige `current` ∈ {nv1,nv2},
   donc suit le skip.

Aucun comportement proposé. Robin arbitre (question métier plus bas).

---

## Anomalie 2 — trou `valide_nv1` dans l’historique

### 5. Deux chemins (et les cinq variantes)

1370 : WV nv1 passé `valide` sans ligne `valide_nv1*`. Historique mesuré :
`assigner_validateur_nv1` + `envoye_nv1` + `modifie`. C’est le chemin
**envoi dépôt sans auto-validation** (`DepotFactureController` L2337–2338
`envoye_nv1`) puis **validation depuis la fiche**.

`FactureController::processValider` L1189–1195 appelle

    validatePendingStep($facture, $user, $commentaire, null, false, …)

5e argument `withDepotHistorique = false` (défaut du service L53).
Alors `syncDepotAfterValidation` (qui seul écrit `valide_nv1`, L166–168
+ L313) **n’est pas appelé**. Le WV est quand même passé `valide` et
`apresNv1Valide` ouvre la NV2 (L124–126).

1315 : `valide_nv1_a_l_envoi` + `envoye_nv2` = déposant = validateur +
intention `valider_nv1` (`DepotFactureController` L2323–2326).

Variante | Fichier | Chemin
`valide_nv1` | `FactureValidationService` L313 | `withDepotHistorique = true` : `/validations` (`ValidationController` L325–330, 5e arg `true`) et validation de masse (`validateGroupe` L267–272, `true`)
`valide_nv1_a_l_envoi` | `DepotFactureController` L2318 et L2325 | Envoi dépôt, validateur = déposant, intention `valider_nv1`
`valide_nv1_compta_deja_reglee` | `DepotFactureValiderEtComptabiliserService` L154–157 | Compta valide une déjà réglée encore en NV1
`valide_nv1_deja_valide` | `DepotFactureValiderDejaValideService` L396 | Circuit « déjà validée » à l’envoi
`valide_historique` | `DepotFactureController` L2763–2766 | Saisie historique payée (n’est PAS un `valide_nv1*`)

Chemins qui valident le WV nv1 **sans** `valide_nv1*` :

- Fiche `/factures/fiche/{id}/valider` : `withDepotHistorique = false`
- Saisie dépôt « Valider l’étape » `DepotFactureController` L1853–1858 :
  même `false`

Cohérent avec 1370 : envoi `envoye_nv1` à 10:16:48 (WV créé), validation
fiche ~2 min plus tard (WV `date_validation` 10:18:21, NV2 ouverte),
sans ligne historique de validation.

### 6. Cause de l’anomalie 1 ?

**Non — défaut indépendant.** Preuve : la frise ignore
`facture_historique`. Si le trou d’historique causait les chevrons gris,
1315 (historique complet) et 1370 se comporteraient pareil sur la frise
dès lors que les WV sont identiques. Or 1315 a `deja_reglee = 0` : type
`standard`, NV2 n’est pas skipped, `current = nv2`, bouton visible.
Seul le flag distingue les deux.

---

## Anomalie 3 — décalage 2 h

### 7. Timezone et types de colonnes

- `config/app.php` L68 : `'timezone' => 'UTC'`.
- `config/database.php` connexion `mysql` : **pas** de clé `timezone`
  (session MySQL = défaut serveur, souvent SYSTEM = Europe/Paris sur OVH).
- `depot_factures.created_at` / `updated_at` : `$table->timestamps()` →
  TIMESTAMP, écriture Eloquent `now()` (UTC).
- `workflow_validations` : idem `$table->timestamps()` +
  `date_assignation` / `date_validation` TIMESTAMP, Eloquent `now()`.
- `facture_historique.created_at` : TIMESTAMP `useCurrent()`
  (`2026_05_12_160000` L19). Modèle `FactureHistorique` :
  `$timestamps = false` (L12). `Historisable::logHistorique` L27–37
  n’envoie **pas** `created_at` → DEFAULT `CURRENT_TIMESTAMP` (horloge
  de session MySQL).

Même événement 1370 : WV / dépôt 10:32:04 (Eloquent UTC écrit naïf) vs
historique `modifie` 12:32:04 (DEFAULT session Paris, CEST = UTC+2).
Écart constant 2 h **établi par le code + les horodatages Robin**.

`modifie` = `DepotFactureController::logHistoriqueIfSaisieChanged` L1818
lors d’un Enregistrer saisie (coche `deja_reglee` après coup, sans
toucher au workflow — voir question métier).

### 8. Délais tableau de bord compta

`ComptaTableauBordService::ageMaxJoursSql` : `TIMESTAMPDIFF(DAY,
MIN(depot_factures.created_at), now())` (granularité **jour**, colonne
Eloquent dépôt, `now()` applicatif UTC). Un biais de 2 h ne change le
compteur **que** si MIN(created_at) tombe dans les 2 h autour d’un
changement de jour. Pas de décalage systématique « 55 j → 55 j + 2 h »
à l’affichage. Non mesuré en prod (pas MySQL ici) ; conclusion **code
seulement**.

---

## Reliquats #1486 (pas un bug de ce lot)

`FactureWorkflowRealignementService::factureAvecNv2()` L390–398 :

    if (FacturePasseParValidationNv2::check($facture)) {
        return true;
    }
    return $facture->workflowValidations->contains(fn ($w) => $w->niveau === 'nv2');

Un WV nv2 déjà ouvert est **conservé** même si le helper #1486 dirait
désormais « pas de NV2 » (programme HECTARE). 1634 et 1735 = reliquats
attendus (#1486 LEÇON). 1315 = pur reliquat enseigne (`deja_reglee = 0`).
1370 = reliquat **plus** flag déjà réglée posé après coup.

---

## Question métier — cocher « déjà réglée » en NV2

Code : `DepotFactureController::applyDepotSaisieFromValidated` écrit
`deja_reglee` sur le dépôt + ocr_raw (L1643, L1667) puis
`logHistoriqueIfSaisieChanged(..., 'modifie')`. 
`synchroniserFlagsDepotDepuisRequete` L4740–4745 : `save()` des flags,
**aucun** appel workflow.

Pas de fermeture de WV, pas de changement de `factures.statut`, pas de
`valide_nv2`. Rien du tout sur le workflow.

Le workflow n’est pas rejoué. Seul le **rendu** de frise / bouton change
au prochain chargement (type `deja_reglee` → skip).

NE PAS proposer de comportement. Robin arbitre.

---

## SQL pour Robin (aucun chiffre ici)

-- A2 — WV nv1 valide sans historique valide_nv1*
SELECT
  factures.id AS facture_id,
  factures.reference,
  factures.statut AS facture_statut,
  depot_factures.id AS depot_id,
  depot_factures.statut AS depot_statut,
  depot_factures.deja_reglee,
  programmes.libelle AS programme_libelle,
  programmes.enseigne_id,
  enseignes.code AS enseigne_code,
  marches.intitule AS marche_intitule,
  fournisseurs.nom AS fournisseur_nom,
  workflow_validations.id AS wv_nv1_id,
  workflow_validations.date_validation AS wv_nv1_date_validation
FROM workflow_validations
INNER JOIN factures
  ON factures.id = workflow_validations.facture_id
 AND factures.deleted_at IS NULL
INNER JOIN depot_factures
  ON depot_factures.facture_id = factures.id
 AND depot_factures.deleted_at IS NULL
 AND depot_factures.id = (
   SELECT MAX(df2.id) FROM depot_factures df2
   WHERE df2.facture_id = factures.id AND df2.deleted_at IS NULL
 )
LEFT JOIN programmes ON programmes.id = factures.programme_id
LEFT JOIN enseignes ON enseignes.id = programmes.enseigne_id
LEFT JOIN marches ON marches.id = factures.marche_id
LEFT JOIN fournisseurs
  ON fournisseurs.id = COALESCE(depot_factures.fournisseur_id, factures.fournisseur_id)
WHERE workflow_validations.niveau = 'nv1'
  AND workflow_validations.statut = 'valide'
  AND NOT EXISTS (
    SELECT 1
    FROM facture_historique fh
    WHERE fh.facture_id = factures.id
      AND fh.action LIKE 'valide_nv1%'
  )
ORDER BY factures.id;

-- Reliquats #1486 — dépôts encore en NV2, programme ≠ ENVOL
SELECT
  depot_factures.id AS depot_id,
  depot_factures.facture_id,
  depot_factures.statut,
  depot_factures.deja_reglee,
  programmes.id AS programme_id,
  programmes.libelle AS programme_libelle,
  programmes.enseigne_id,
  enseignes.code AS enseigne_code,
  marches.intitule AS marche_intitule,
  fournisseurs.nom AS fournisseur_nom
FROM depot_factures
INNER JOIN factures
  ON factures.id = depot_factures.facture_id
 AND factures.deleted_at IS NULL
LEFT JOIN programmes ON programmes.id = factures.programme_id
LEFT JOIN enseignes ON enseignes.id = programmes.enseigne_id
LEFT JOIN marches ON marches.id = factures.marche_id
LEFT JOIN fournisseurs
  ON fournisseurs.id = COALESCE(depot_factures.fournisseur_id, factures.fournisseur_id)
WHERE depot_factures.deleted_at IS NULL
  AND depot_factures.statut = 'en_validation_nv2'
  AND (
    factures.programme_id IS NULL
    OR enseignes.code IS NULL
    OR UPPER(TRIM(enseignes.code)) <> 'ENVOL'
  )
ORDER BY depot_factures.id;

-- deja_reglee encore en NV1 ou NV2
SELECT
  depot_factures.id AS depot_id,
  depot_factures.facture_id,
  depot_factures.statut,
  depot_factures.deja_reglee,
  programmes.libelle AS programme_libelle,
  programmes.enseigne_id,
  enseignes.code AS enseigne_code,
  marches.intitule AS marche_intitule,
  fournisseurs.nom AS fournisseur_nom
FROM depot_factures
INNER JOIN factures
  ON factures.id = depot_factures.facture_id
 AND factures.deleted_at IS NULL
LEFT JOIN programmes ON programmes.id = factures.programme_id
LEFT JOIN enseignes ON enseignes.id = programmes.enseigne_id
LEFT JOIN marches ON marches.id = factures.marche_id
LEFT JOIN fournisseurs
  ON fournisseurs.id = COALESCE(depot_factures.fournisseur_id, factures.fournisseur_id)
WHERE depot_factures.deleted_at IS NULL
  AND depot_factures.deja_reglee = 1
  AND depot_factures.statut IN ('en_validation_nv1', 'en_validation_nv2')
ORDER BY depot_factures.id;

## DÉPLOIEMENT-TEST

Aucun PHP applicatif. Après `git pull` : lire cette fiche. Exécuter les
trois requêtes. Coller les résultats avant tout lot de fix.
Pas de `journal:sync`. Pas de npm/composer/optimize.
Ne pas modifier les dépôts 1634 et 1735.

## LEÇON

`deja_reglee` n’est pas qu’un badge : c’est un type de frise qui **saute**
NV1/NV2. Le poser après ouverture d’un WV nv2 rend l’écran muet tout en
laissant le badge « Validation NV2 ». Les droits admin ne compensent pas
un `currentStepKey` vide. L’historique `valide_nv1` est un autre circuit
(`withDepotHistorique`) : ne pas s’en servir pour expliquer la frise.
