---
suivi: 1039
date: 2026-07-29
sujet: Bilan Facturé — preuve NV1 double (workflow ou statut aval)
chantier: budgets-programme
type: fix
statut: poussé
hash: 0442ab49
fichiers:
  - app/Models/DepotFacture.php
  - tests/Unit/BilanFiltreNv1SourceUniqueTest.php
  - tools/diag/diag_bilan_1039.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
---

## PROMPT ENVOYÉ

SUIVI #1039 — suite de la régression #1036 :
- conserver une source unique PHP+SQL pour “NV1 franchie” ;
- admettre 2 preuves équivalentes :
  - P1 : workflow `nv1/valide`,
  - P2 : statut aval qui ne peut pas être atteint sans NV1 franchie ;
- garder les exclusions légitimes (nv1 en attente, facture_id NULL, rejetés, archivés, soft-delete) ;
- corriger le script de contrôle pour refléter la règle canonique.

## SYNTHÈSE

### Point unique de la règle (inchangé dans l’architecture, enrichi en logique)

La définition canonique reste **uniquement** dans `DepotFacture` :
- `compteDansFactureApresNv1Valide()` (chemin objet PHP),
- `sqlConditionCompteDansFactureApresNv1Valide()` (chemin SQL agrégé).

Aucune liste parallèle ajoutée dans services/controllers/scripts.

### Changement métier implémenté (#1039)

La règle “NV1 franchie” devient :
- vrai si P1 : présence d’une ligne `workflow_validations` `niveau='nv1'` et `statut='valide'`,
- ou si P2 : le statut **normalisé** est dans les statuts avals prouvant le franchissement :
  - `nv2_en_cours`,
  - `a_comptabiliser`,
  - `comptabilisee`,
  - `mise_en_paiement`,
  - `en_paiement`,
  - `payee`.

Implémentation :
- nouvelle méthode `DepotFacture::statutsNormalisesPreuveNv1Franchi()`,
- utilisée **à la fois** par PHP et SQL.

### Tableau enum → “prouve NV1 franchie ?” (fondé sur le code)

#### `factures.statut`

| Statut | Prouve NV1 franchie ? | Justification code |
|---|---|---|
| `recue` | Non | état initial avant validation (`FactureValidationService` valide des workflows en attente ; aucune transition vers aval). |
| `en_validation_nv1` | Non | étape NV1 en cours (`FactureValidationService` valide la ligne nv1 pour passer ensuite à l’aval). |
| `en_validation_nv2` | Oui | atteint après `apresNv1Valide()` (`FactureWorkflowApresNv1Service` : nv1 validée puis passage NV2). |
| `a_comptabiliser` | Oui | atteint après NV1/NV2 (`FactureWorkflowApresNv1Service::apresNv1Valide` FG, `apresNv2Valide` programme). |
| `comptabilisee` | Oui | état aval post-compta (`FactureWorkflowService`), donc après chaîne de validation. |
| `en_paiement` | Oui | état aval après compta/mise en paiement (`FactureWorkflowService`). |
| `payee` | Oui | état final paiement (`FactureWorkflowService` / historique). |
| `rejetee` | Non (exclu) | garde-fou explicite dans `compteDansFactureApresNv1Valide()` et SQL (`normalized='rejetee'`). |

#### `depot_factures.statut`

| Statut | Prouve NV1 franchie ? | Justification code |
|---|---|---|
| `depose` | Non | état de dépôt initial, avant facture validée. |
| `attribue` | Non | état de préparation/saisie, pas de preuve NV1. |
| `en_analyse` | Non | étape OCR/analyse, avant workflow de validation. |
| `analyse` | Non | idem. |
| `en_cours_saisie` | Non | facture non finalisée / non validée. |
| `en_validation_nv1` | Non | NV1 en cours par définition. |
| `en_validation_nv2` | Oui | synchronisé après NV1 validée (`FactureWorkflowApresNv1Service::apresNv1Valide`). |
| `a_comptabiliser` | Oui | aval après validations (`FactureWorkflowApresNv1Service`). |
| `comptabilisee` | Oui | aval compta (`FactureWorkflowService` + sync dépôt). |
| `valide` | Oui, **si facture liée** | normalisé en `comptabilisee` dans `FactureStatutService` ; dans #1039, la branche non-historique exige `facture_id IS NOT NULL`. |
| `paye` | Oui, **si facture liée** | normalisé en `payee` ; dans #1039, `facture_id IS NOT NULL` requis hors historique. |
| `rejete` | Non (exclu) | garde-fou explicite. |
| `archive` | Non (exclu) | garde-fou explicite. |

#### `workflow_validations`

| Champ | Valeur | Prouve NV1 franchie ? | Justification code |
|---|---|---|---|
| `niveau` | `nv1` + `statut=valide` | Oui (P1) | preuve directe utilisée dans `aWorkflowNv1Valide()` / SQL EXISTS. |
| `niveau` | `nv1` + `statut=en_attente` | Non | NV1 non franchie (cas demandé “13 exclus”). |
| `niveau` | `nv1` + `statut=rejete` | Non | rejet de la validation. |
| `niveau` | `nv2` / `mise_paiement` / `paiement_effectif` / `comptabilisation` | Indirectement aval | ces niveaux apparaissent après séquence de validations ; la règle #1039 retient ce signal via le statut normalisé P2, pas via un EXISTS sur ces niveaux. |

### Audit des consommateurs de la règle

Appelants runtime de `sqlConditionCompteDansFactureApresNv1Valide()` / `compteDansFactureApresNv1Valide()` :
1. `app/Services/BilanFinancierService.php`
   - `buildFactureNv1ByMarche()`
   - `buildDepotsHorsMarcheKpiParPoste()`
   - Impact #1039 : récupère les factures aval historiques sans `nv1/valide`.
2. `app/Models/DepotFacture.php`
   - `sommeMontantHtFactureApresNv1PourProgramme()`
   - Impact #1039 : dashboard aligné sur la même règle.
3. `app/Http/Controllers/ProgrammeDashboardController.php`
   - via `sommeMontantHtFactureApresNv1PourProgramme()`.
4. Scripts diag
   - nouveau `tools/diag/diag_bilan_1039.php` (lecture seule, contrôle dédié).

Conclusion audit : **bilan + dashboard restent cohérents**, car même condition canonique SQL.

### Script de contrôle

Choix fait : **nouveau script** `tools/diag/diag_bilan_1039.php` (sans écraser #1035).

Le script affiche explicitement :
- comparaison #1035 vs #1036 strict vs #1039,
- vérification de la population “98 dépôts” issue du diagnostic #1037,
- checks ciblés :
  - `depot#12` attendu inclus,
  - `depot#1212` attendu inclus,
  - `depot#1206` attendu exclu,
- et le contrôle attendu :
  - 42 inclus (no proof paid),
  - 13 exclus (nv1 en attente),
  - 43 exclus (`facture_id` NULL),
  - exclusions légitimes attendues : 56, ΣHT 129 530,32.

### Journal #412

Entrée ajoutée (même commit) car impact visible sur les montants du Bilan/Facturé.

## DÉPLOIEMENT-TEST

1. `git pull origin main`
2. `find app/ -name "*.php" -exec touch {} +`
3. `php artisan view:clear`
4. `php tools/diag/diag_bilan_1039.php`
5. Vérifier dans la sortie :
   - 42/13/43,
   - `depot#12` inclus, `depot#1212` inclus, `depot#1206` exclu.
6. Si entrée journal déployée :
   - `php artisan journal:sync`

## LEÇON

Une règle métier ne peut pas dépendre d’une preuve technique (ligne de workflow) qui n’a pas toujours été historisée. La correction robuste consiste à accepter les **preuves équivalentes** dans une **seule définition canonique** partagée par les chemins PHP et SQL.
