---
suivi: 1450
date: 2026-09-04
sujet: Dépôt facture — enregistrement permissif, valideur par défaut, filtre à compléter
chantier: depot_factures
type: feature
statut: poussé
hash: f33abb92
fichiers:
  - app/Http/Controllers/DepotFactureController.php
  - resources/js/Pages/DepotFactures/DepotFactureSaisieForm.vue
  - resources/js/Pages/DepotFactures/Index.vue
  - tests/Unit/DepotFacture1450SourceTest.php
  - docs/suivi/SUIVI_1450_depot_enregistrement_permissif_filtre_valideur.md
  - public/build/
---

# SUIVI #1450 — Enregistrement permissif + filtre valideur

## PROMPT ENVOYÉ

Quatre volets liés sur le dépôt de facture : valideur par défaut = déposant
(JSON `ocr_raw.saisie.validateur_id`, sans colonne) ; enregistrement permissif
vs envoi en validation renforcé (rattachement budgétaire complet) ; champs
indépendants saisissables sans destination (PROJETS fermé) ; filtre cumulable
« Mes factures à compléter » (valideur, encore en boîte), coché par défaut.

## FICHES LUES

- `SUIVI_1392` LEÇON — case hors ERP opt-in ; coerce avant assert ; `allow-hors-erp-in-search` conservé.
- `SUIVI_1407` LEÇON — ancienneté couleurs, Par entité (`sans`), recherche libre, pagination 30 : non défaits. « Voir mes dépôts » n’est plus coché par défaut (choix #1450).
- `SUIVI_1435` LEÇON — contrat `{champ, valeur}` (avis d’imposition) : non touché. Aucune édition JSON « objet entier ».
- `SUIVI_1444` LEÇON — « Voir le doublon » + pas d’`overflow-x-hidden` : bouton conservé, aucun overflow ajouté.

Base rebuild : `origin/main` `c893f9cb` (chore hash fiche #1452), après
`git fetch`. Implémentation commencée sur `4941316f` (#1451) ; #1449 et #1452
(parcelle) sont déjà sur main. Rebuild **après** #1452 pour ne pas écraser
`public/build/`. `origin/main..HEAD` vide avant ce lot.

Journal utilisateur : entrée `depot-enregistrement-permissif-filtre-valideur-1450-2026-09-04`
déjà présente sur `origin/main` (commit #1451). Pas de doublon (#1406).

## MAPPING UI ↔ BACK

| Libellé affiché | Valeur postée | Colonne / JSON | Back |
|---|---|---|---|
| Mes factures à compléter | `a_completer=1\|0` | `ocr_raw.saisie.validateur_id` (+ email ; déposant si vide) | défaut **true** si param absent |
| Voir mes dépôts | `mes_depots=1\|0` | `depot_factures.user_id` | défaut **false** si param absent |
| Les deux cochées | union (OR) | — | `user_id = moi OR valideur JSON = moi` |
| Par type / validateur / entité / recherche | inchangés #1407 | — | AND par-dessus l’union |
| Valideur (formulaire) | `validateur_id` / `validateur_email` | `ocr_raw.saisie.*` | défaut création = azure_id déposant sinon `users.id` |
| Enregistrer | PATCH brouillon | whitelist `depotDraftSaisieValidationRules` | permissif |
| Envoyer en validation | POST valider | `depotFactureValiderValidationRules` + `assertRattachementBudgetCompletPourEnvoi` | strict |

## RÈGLES D’ENREGISTREMENT (retirées vs conservées)

Fichier : `DepotFactureController::depotDraftSaisieValidationRules` (~L858–951).

Retiré du brouillon (étaient exigés comme à l’envoi) :

- `programme_id` `required_if:destination,PROGRAMMES` → `nullable` + exists
- `categorie_id` `required_if:destination,FRAIS_GENERAUX` → `nullable` + exists
- `date_reglement` / `mode_reglement_direct` `required_if:deja_reglee` / `saisie_historique` → `nullable` seulement
- Plus de merge silencieux `destination=PROGRAMMES` si vide (`mergeDestinationDepuisSaisieSansDefautProgramme`)

Conservé (minimum technique, ligne relisible) : types, `exists`, `nullable`, whitelist champs, `validateur_*` nullable, montants nullable, ventilations si présentes.

`assertRattachementProgrammeBudgetValide(..., brouillonPermissif: true)` à l’enregistrement : ne bloque plus un poste direct sans id.

## RÈGLES D’ENVOI (renforcées)

`depotFactureValiderValidationRules` (~L958+) : tout l’existant **plus** `destination` **required** (`PROGRAMMES` \| `FRAIS_GENERAUX`).

Nouveau `assertRattachementBudgetCompletPourEnvoi` après coerce hors ERP :

- FRAIS_GENERAUX : OK (catégorie déjà `required_if`)
- sinon destination ≠ PROGRAMMES : 422 `destination`
- PROGRAMMES : il faut `sans_marche` **ou** `marche_id` **ou** poste (`poste_budgetaire_type_id` / `programme_budget_id` / `rattachement_poste_direct`)
- sinon 422 `marche_id` : `Rattachement budgétaire incomplet : il manque un marché, un poste budgétaire (ou une enveloppe), la case « marché non encore créé ». Sans l’un de ces trois, la facture est incomptabilisable.`

Appels : `valider`, `validerHistorique`, payloads déjà-validé / valider-et-comptabiliser.

## CHOIX FILTRES

- « Mes factures à compléter » : **cochée par défaut**.
- « Voir mes dépôts » : **décochée par défaut** (était cochée au #1407) — sinon l’union à l’ouverture noierait la file « à compléter ».
- Les deux cochées = **UNION** (OR), documenté à l’écran.
- Filtre JSON sans index : dette si le volume de la boîte croît. Colonne `validateur_id` **recommandée** mais **non créée** (arbitrage Robin).

## RATTRAPAGE (simulation — Robin, ne pas exécuter l’UPDATE sans relire le SELECT)

```sql
-- SIMULATION
SELECT
  df.id,
  df.uuid,
  df.statut,
  df.user_id,
  u.email AS deposant_email,
  u.azure_id AS deposant_azure_id,
  JSON_UNQUOTE(JSON_EXTRACT(df.ocr_raw, '$.saisie.validateur_id')) AS validateur_id_actuel,
  JSON_UNQUOTE(JSON_EXTRACT(df.ocr_raw, '$.saisie.validateur_email')) AS validateur_email_actuel
FROM depot_factures df
INNER JOIN users u ON u.id = df.user_id
WHERE df.deleted_at IS NULL
  AND df.statut IN ('depose', 'attribue', 'en_analyse', 'analyse', 'en_cours_saisie')
  AND (
    JSON_EXTRACT(df.ocr_raw, '$.saisie.validateur_id') IS NULL
    OR JSON_UNQUOTE(JSON_EXTRACT(df.ocr_raw, '$.saisie.validateur_id')) IN ('', 'null')
  )
  AND IFNULL(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(df.ocr_raw, '$.saisie.validateur_email'))), ''), '') = '';

-- UPDATE (après validation du SELECT ; JSON_SET ne remplace pas le reste de saisie)
UPDATE depot_factures df
INNER JOIN users u ON u.id = df.user_id
SET df.ocr_raw = JSON_SET(
  IF(
    JSON_EXTRACT(df.ocr_raw, '$.saisie') IS NULL,
    JSON_SET(IFNULL(df.ocr_raw, JSON_OBJECT()), '$.saisie', JSON_OBJECT()),
    df.ocr_raw
  ),
  '$.saisie.validateur_id',
  IF(u.azure_id IS NOT NULL AND TRIM(u.azure_id) <> '', u.azure_id, CAST(u.id AS CHAR)),
  '$.saisie.validateur_email',
  IFNULL(u.email, '')
)
WHERE df.deleted_at IS NULL
  AND df.statut IN ('depose', 'attribue', 'en_analyse', 'analyse', 'en_cours_saisie')
  AND (
    JSON_EXTRACT(df.ocr_raw, '$.saisie.validateur_id') IS NULL
    OR JSON_UNQUOTE(JSON_EXTRACT(df.ocr_raw, '$.saisie.validateur_id')) IN ('', 'null')
  )
  AND IFNULL(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(df.ocr_raw, '$.saisie.validateur_email'))), ''), '') = '';
```

## SYNTHÈSE

Valideur posé au déposant à la création si vide. Enregistrer sauve l’incomplet.
Envoyer exige un rattachement budgétaire. Champs indépendants hors destination.
Filtre « à compléter » JSON, union avec « mes dépôts ». `FactureVisibility` et
`ComptabilisationController` non modifiés. Destination PROJETS toujours fermée.

## DÉPLOIEMENT-TEST

Après `git pull` : `php artisan optimize:clear` et `view:clear`. Pas de migrate.
`php artisan journal:sync` seulement si l’entrée #1450 n’est pas encore en base
(elle a pu partir avec le #1451).

## LEÇON

Le valideur n’est pas une colonne : filtrer `ocr_raw.saisie.validateur_id` sans
index. Sur 39 dépôts c’est indolore ; au-delà, extraire une colonne. Ne pas
assouplir l’enregistrement sans durcir l’envoi (factures NV1 incomptabilisables).
Deux cases « mes * » : union documentée à l’écran, un seul défaut coché.
Un stash / un autre lot front (`public/build`) peut effacer un WIP : sérialiser
le rebuild, ne jamais mélanger parcelle et dépôt dans le même commit.
`depotDraftSaisieValidationRules` avait un `return` avant `assouplirModeReglementDirectSiOverrideAdmin` (#985) : restauré via `$rules` puis assouplissement.
