---
suivi: 1392
date: 2026-09-02
sujet: depot_facture_programme_hors_erp
chantier: depot_factures
type: feature
statut: poussé
hash: 30840365
fichiers:
  - app/Support/ProgrammeEnseigneComfortFiltre.php
  - app/Support/ProgrammeHorsErpGuard.php
  - app/Support/ProgrammeSaisieSelectPresenter.php
  - app/Http/Controllers/ProgrammeController.php
  - app/Http/Controllers/DepotFactureController.php
  - resources/js/Components/Programmes/ProgrammeSearchSelect.vue
  - resources/js/Pages/DepotFactures/DepotFactureSaisieForm.vue
  - tests/Unit/ProgrammeHorsErp1392SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - docs/suivi/SUIVI_1392_depot_facture_programme_hors_erp.md
---

## PROMPT ENVOYÉ

SUIVI #1392 — dépôt facture : case « Afficher les anciens programmes hors ERP », pastille recherche, sans_marche forcé + garde-fous serveur. Socle #1391 requis. Ne pas toucher ComptabilisationController / NV2 / « Tout débloquer » enseigne.

## FICHES LUES

- `SUIVI_1391_programme_hors_erp_socle.md` — statut `hors_erp`, garde-fous marché/budget/bilan, pas de migration.

## MESURE PRÉALABLE — `sans_marche` (point C)

| Question | Réponse |
|----------|---------|
| Choix utilisateur ? | Oui — sentinelles « Marché non encore créé » / « rattacher à un poste » dans `DepotFactureSaisieForm.vue` (`MARCHE_SENTINEL_SANS`, `form.sans_marche`, `form.rattachement_poste_direct`). |
| Effets | `horsMarcheProgrammes = sans_marche \|\| rattachement_poste_direct` ; retenues marché off ; `assertRattachementProgrammeBudgetValide` → `assertAucuneRetenueHorsMarche` ; compta 471000 si `sans_marche` (`CompteChargePropositionResolver::COMPTE_ATTENTE_PROGRAMME_SANS_MARCHE`). |
| Forcer pour hors ERP ? | Oui — poste reste optionnel sur `sans_marche` (#1193) ; pas de conflit NV2 (via entité programme). Coerce serveur + payload front. |

## SYNTHÈSE

### Case à cocher recherche programme

| Libellé affiché | Valeur postée | Param back | Effet requête |
|-----------------|---------------|------------|---------------|
| Afficher les anciens programmes hors ERP (cochée) | checkbox true | `include_hors_erp=1` | UNION `statut='hors_erp'` en plus des actifs |
| Décochée (défaut) | absent / 0 | — | `baseActifsQuery()` strict (comportement inchangé) |

### Back

- `ProgrammeEnseigneComfortFiltre::baseActifsQueryWithOptionalHorsErp()` — actifs + optionnel hors_erp, jamais inactifs.
- `ProgrammeController::search` — param `include_hors_erp`, meta JSON.
- `ProgrammeSaisieSelectPresenter` — champ `statut` dans payload.
- `ProgrammeHorsErpGuard::coerceDepotFactureValidated()` + `assertDepotFactureRattachement()` — refus marché/poste/enveloppe, force `sans_marche`.
- `DepotFactureController::applyDepotSaisieFromValidated` + `assertRattachementProgrammeBudgetValide`.

### Front dépôt

- `ProgrammeSearchSelect` : prop `allowHorsErpInSearch`, pastille « Hors ERP ».
- `DepotFactureSaisieForm` : bandeau marché indisponible, `sans_marche` forcé au payload, fetch `include_hors_erp=1` pour programme préchargé.

### Non modifié (garde-fous spec)

- `ComptabilisationController`, `FacturePasseParValidationNv2`, mécanisme « Tout débloquer » enseigne.

## DÉPLOIEMENT-TEST

```bash
git pull origin main
php artisan optimize:clear
php artisan view:clear
php artisan journal:sync
```

1. Dépôt facture — case décochée : recherche identique à avant (#1392 absent des résultats).
2. Cocher « Afficher les anciens programmes hors ERP » → programmes hors ERP visibles avec pastille.
3. Sélectionner un hors ERP → bandeau sans marché, pas de select marché/poste.
4. Enregistrer brouillon → `sans_marche=true`, pas de 422.
5. Tenter POST avec `marche_id` sur programme hors ERP (API) → 422 garde-fou serveur.

## LEÇON

- Case décochée = zéro régression UX ; opt-in explicite pour hors ERP.
- Coerce **avant** assert dans `applyDepotSaisieFromValidated`.
- `include_id` + `include_hors_erp=1` pour recharger un brouillon existant.
