---
suivi: 1551
date: 2026-09-15
sujet: Annuler une validation NV1
chantier: factures / workflow / validation-nv1
type: feature
statut: poussé
hash: 677108c0
fichiers:
  - app/Services/FactureAnnulerValidationNv1Service.php
  - app/Http/Controllers/FactureAnnulerValidationNv1Controller.php
  - app/Http/Controllers/FactureController.php
  - app/Services/TaskSyncService.php
  - app/Services/Tasks/ValidationFactureNv1TaskSync.php
  - app/Services/Tasks/ValidationFactureNv1WorkflowHooks.php
  - app/Models/Task.php
  - app/Support/FactureHistoriquePresenter.php
  - routes/web.php
  - resources/js/Pages/Factures/Show.vue
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - docs/suivi/SUIVI_1551_annuler_validation_nv1.md
---

# SUIVI #1551 — PHASE 2 : annuler une validation NV1

## PROMPT ENVOYÉ

Implémenter l’annulation NV1 selon le diagnostic #1549. Endpoint POST
motif obligatoire, droits m365GroupNames brut, retour
en_validation_nv1, WV nv1 remise en_attente en place, fermeture NV2
ou mise_paiement en_attente, réouverture tâche NV1, historique dédié,
bouton fiche. Pas de migration, pas de touche à l’auto-NV1 ni à NV2
validée ni à la compta. Libération RG non rendue (hors périmètre).

## FICHES LUES

- **#1549 LEÇON** — pas de boucle auto-NV1 (garde facture_id) ; cible
  `en_validation_nv1` ; acte = WV nv1/valide.validateur_id ; update
  in-place pas sans_objet ; upsert NV1 ne ressuscite pas une tâche
  réalisée (écart : PHASE 2 ajoute `reopenClosedOrUpsert`).
- **#1535** — libération RG acquise dès NV1+ ; un retour en NV1 à
  valider **reste dans le plafond**. Signalé, non corrigé.
- **#1322 / #1410** — `m365GroupNames()` + `abort_unless`, jamais
  Gate / `hasAnyM365Group` pour le droit d’annuler.

## SYNTHÈSE

Route `POST /factures/fiche/{facture}/annuler-validation-nv1`
(`routes/web.php`, nom `factures.fiche.annuler-validation-nv1`).
Contrôleur dédié (pas `authorize()`, pas Gate).

Droit : `m365GroupNames()` brut — `GS_COMPTABILITE` ou
`GS_DIRECTION_GENERALE`, ou `users.id` = `workflow_validations.validateur_id`
de la ligne nv1/`valide`. Pas `ocr_raw`, pas `GS_ADMIN_ERP` seul.

Conditions serveur (bouton = même calcul) : nv1 valide, pas de nv2
valide, statuts hors compta/paiement/payé/rejet/archive, pas de
`intacct_bill_id`, pas `deja_valide` / `deja_reglee` /
`saisie_historique`. Hors ENVOL / FG : possible tant que pas compta
(ferme `mise_paiement`/`en_attente`).

Défaire : nv1 → `en_attente` (date_validation et commentaire vidés,
validateur_id conservé) ; facture + dépôt → `en_validation_nv1` ;
DELETE des lignes nv2/`en_attente` et mise_paiement/`en_attente`
(pas `sans_objet`) ; tâche NV2 fermée
(`annulation_validation_nv1`) ; tâche NV1 rouverte (méthode dédiée,
car `upsertFromEvent` refuse de ressusciter une tâche réalisée) ;
alerte « à comptabiliser » retirée si elle existait ;
`facture_historique` action `annule_validation_nv1`.

Bouton « Annuler la validation » sur `Factures/Show.vue`, barre
d’actions, indépendant de `can_validate` (valideur NV1 ≠ NV2).
Dialogue motif obligatoire.

### Mapping UI ↔ back

| Libellé affiché | Valeur postée | Colonne / cible | Libellé back |
|-----------------|---------------|-----------------|--------------|
| Annuler la validation | — (ouvre le dialogue) | `permissions.can_annuler_validation_nv1` | bouton fiche |
| Motif * | `motif` | `facture_historique.commentaire` | « Motif : … » dans l’historique |
| — | — | `facture_historique.action` = `annule_validation_nv1` | Validation NV1 annulée |
| — | — | `workflow_validations.statut` nv1 → `en_attente` | frise NV1 à valider |
| — | — | `factures.statut` / `depot_factures.statut` → `en_validation_nv1` | badge NV1 |

## DÉPLOIEMENT-TEST

OVH : `git pull origin main` ; `php artisan optimize:clear` ;
`php artisan view:clear` ; `php artisan route:clear` ;
`php artisan journal:sync`. Pas de migrate. Pas de npm sur le serveur.

Cas à tester en prod (sqlite skipped) :

1. ENVOL NV1 valide, NV2 en attente → bouton, motif, retour NV1,
   tâche NV2 fermée, tâche NV1 rouverte.
2. HECTARE / FG à comptabiliser → bouton, mise_paiement fermée.
3. NV2 déjà validée → pas de bouton, POST 422.
4. Comptabilisée → pas de bouton, POST 422.
5. Déjà réglée / déjà validé → pas de bouton, POST 422.
6. Compte hors droits → 403 (y compris GS_ADMIN_ERP seul).
7. Re-valider après annulation → une seule ligne nv1, pas de doublon.
8. NV1 automatique à l’envoi → annulation possible, pas de re-auto-NV1.
9. Historique : « Validation NV1 annulée » + motif.

Mémoire RG : la libération reste décomptée du plafond (#1535).

## LEÇON

`TaskSyncService::upsertFromEvent` ne rouvre jamais une tâche
réalisée/annulée. Pour un vrai retour en NV1, il faut une méthode
dédiée (`reopenClosedOrUpsert`). Fermer l’aval = DELETE des WV
`en_attente` (nv2 / mise_paiement), pas `sans_objet`, pour que la
frise redevienne identique. Le droit d’annuler se lit sur
`validateur_id` de la ligne nv1/`valide`, pas sur le désigné OCR.
L’auto-NV1 à l’envoi ne se rejoue pas (`facture_id` déjà posé).
