---
suivi: 1483
date: 2026-09-08
sujet: Correction écarts tableau de bord compta (banque, TTC, sources uniques, pipeline)
chantier: comptabilite / synthese
type: feature
statut: poussé
hash: 763f3951
fichiers:
  - app/Services/Compta/ComptaTableauBordService.php
  - app/Support/Compta/ComptaTableauBordAcces.php
  - app/Http/Controllers/ComptaTableauBordController.php
  - app/Services/FactureGlobaleListeQuery.php
  - app/Http/Controllers/FactureGlobaleController.php
  - resources/js/Pages/Factures/TableauBord.vue
  - public/build/
  - tests/Unit/ComptaTableauBord1478Test.php
  - tests/Unit/ComptaTableauBord1478SourceTest.php
  - tests/Feature/ComptaTableauBord1478FeatureTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - docs/suivi/SUIVI_1483_fix_ecarts_tableau_bord_compta.md
---

# SUIVI #1483 — PHASE 2 CORRECTION — écarts tableau de bord compta

## PROMPT ENVOYÉ

Correction des quatre défauts établis au #1482 : triple non_rattache,
à payer en HT, sources dupliquées, pipeline désaligné des écrans.
Avoirs inclus + ligne séparée. Décision Robin TTC décaissement / HT
amont. Pas de modification ComptabilisationController / DepotFactureController.

## BASE GIT

```
git log --oneline -1 origin/main
5a418779 chore(suivi): hash fiche #1482
```

(Attendu prompt : `5a418779` — conforme. Pas de rebase.)

MySQL agent : refusé. Aucun chiffre de production inventé.

## FICHES LUES

- **#1482** (LEÇON) — grouper sur un id et afficher un autre COALESCE
  produit des jumeaux de libellé ; aPayerJoinQuery copiait
  eligibleFacturesQuery ; 147 vs 146 écarté par mesure (pas de doublon).
  Impact : une expression de clé, consommer eligibleFacturesQuery, pas
  de COUNT(DISTINCT) cosmétique.
- **#1478** (LEÇON) — totaux socle, clic hors /comptabilisation pour la
  DG. Impact : clic conditionnel (compta/admin → écran, DG → file_compta=1).
- **#1467** — file = scopeEligibleComptabilisation (lu, non modifié).

## SYNTHÈSE

### 1. Bloc bancaire — mapping AVANT / APRÈS

Source | Expression clé | Expression libellé | Cas Non rattaché | AVANT | APRÈS
ocr_raw.mise_en_paiement.financial_entity_intacct_id | 1er COALESCE de la clé | — | non | override pouvait être la clé alors que le label tombait sur fe.name NULL | même rang dans la clé ; le label suit la clé
fe_programme.intacct_id | 2e | name en priorité sur le label | non | clé 512900 + label non_rattache si name NULL | clé 512900 + label name ou « Compte 512900 »
Défaut E-04→512900 / E-13→512901 | 3e | absent du label AVANT | non | clé valide, label repli | même id dans la clé, label formaté
Aucune source | littéral interne non_rattache | « Non rattaché » | oui | trois cartes | un seul groupe « Non rattaché »

GROUP BY : compte_key + est_avoir + nature + origine + bucket (plus le label).
Libellé SQL = CASE clé=non_rattache THEN « Non rattaché » ELSE COALESCE(fe.name, clé).
PHP : jamais de snake_case à l’écran ; un id nu devient « Compte {id} ».

### 2. HT / TTC

Bloc | Unité | Colonne source | Mention affichée | Justification
En dépôt, NV1, NV2, À comptabiliser | HT | COALESCE(factures.montant_ht, depot_factures.montant_ht) | « HT » | suivi budgétaire (décision Robin)
À payer, bloc bancaire | TTC | COALESCE(factures.net_a_payer_ttc, factures.montant_ttc, depot_factures.montant_ttc) | « TTC » | décaissement
Échues | TTC | COALESCE(factures.net_a_payer_ttc, factures.montant_ttc) | « TTC » | décaissement
Graphique | HT ou TTC selon l’étape | même que la carte | infobulle + unité | pas de montant nu

Colonne TTC retenue : `factures.net_a_payer_ttc`.
Preuve écran MEP : `PaiementLignesPayablesService` affiche `montant_net` calculé par
`Net401DepuisLignesComptables` (crédit 401 des lignes comptables) ± override
`ocr_raw.mise_en_paiement.lignes.{lineKey}.montant_net`. Ce n’est pas une
colonne SQL. `Facture::recalculer()` écrit le net après RG et CP dans
`net_a_payer_ttc`. `RapprochementTresorerieService` lit la même colonne.
Le dashboard agrège des FACTURES (eligibleFacturesQuery), pas des lignes
MEP (collectLineStubs ignore un bill_id vide). On ne reconstitue pas le
401 en JSON_TABLE (risque de jointure 1:N, sqlite, overrides).

### 3. Pipeline — mapping clics

Carte | Prédicat AVANT | Prédicat APRÈS | URL AVANT | URL APRÈS | Périmètre = compteur | Groupe URL
En dépôt | 5 statuts brut, historique false OU NULL | statutsStadeDepot() + NOT avoir (onglet) | /factures?tab=en_depot | identique | oui (onglet) | les 3
NV1 | statut brut en_validation_nv1 | FactureGlobaleEtapeStatutFilter::applyEtape('nv1') | f_statut=nv1 | identique | oui | les 3
NV2 | statut brut en_validation_nv2 | applyEtape('nv2') | f_statut=nv2 | identique | oui | les 3
À comptabiliser | depot.statut=a_comptabiliser | DepotFacture::eligibleComptabilisation() (lu) | f_statut=a_comptabiliser | /factures/comptabilisation ou ?file_compta=1 | oui | compta/admin vs DG
À payer | copie aPayerJoinQuery | eligibleFacturesQuery + jointure MAX(id) | /factures/mise-en-paiement | identique | ensemble factures = query ; l’écran MEP peut afficher moins de LIGNES (bill_id vide) — écran non modifié | les 3 (MEP autorise la DG)

Tableau droits clic À comptabiliser (aucun 403) :

Groupe | Accès dashboard | Accès /factures/comptabilisation | URL carte
GS_COMPTABILITE | oui | oui (ensureComptabilisationUser) | /factures/comptabilisation
GS_ADMIN_ERP | oui | oui | /factures/comptabilisation
GS_DIRECTION_GENERALE | oui | non (403) | /factures?tab=tous&file_compta=1
GS_DSI | non | oui | n’ouvre pas cette page

file_compta=1 applique eligibleComptabilisation sur la liste globale
(paramètre opt-in ; sans lui la liste est inchangée).

Blocages / échues : pas de filtre liste existant pour inactif / doublon / RIB.
URLs conservées vers l’écran où l’on TRAITE (MEP, nomenclature, liste tous).
Échues : f_echeance_fin + f_statut excluant payee/rejetee (filtres déjà
présents). Carte = table factures ; liste = dépôts — écart structurel dit.

### 4. Sources uniques

Notion | Fonction unique | Consommateurs | Divergences fusionnées
À payer | PaiementLignesPayablesService::eligibleFacturesQuery | carte, banque, pied, RIB manquant | MAX(id) conservé pour joindre le dépôt (latestOfMany('id')) ; saisie_historique = false (écran), plus OR NULL
À comptabiliser | DepotFacture::scopeEligibleComptabilisation | carte + URL file_compta / écran | plus le statut brut seul
NV1 / NV2 | FactureGlobaleEtapeStatutFilter::applyEtape | carte + URL f_statut | plus le statut brut
En dépôt | statutsStadeDepot + NOT avoir | carte + tab=en_depot | aligné onglet

saisie_historique : migration 2026_05_14 boolean()->default(false) SANS
nullable() → NOT NULL DEFAULT 0. Predicat retenu = false (écran MEP).
À confirmer en prod : SHOW COLUMNS FROM depot_factures LIKE 'saisie_historique';

### 5. Avoirs

Bloc | Inclus | Signe | Ligne séparée
À payer (carte) | oui | négatif minore le total | non (total unique)
Bloc bancaire | oui | négatif | oui, clé avoirs / libellé « Avoirs »
Pipeline amont | inchangé (onglet dépôt exclut les avoirs) | — | —

Pied = somme des cartes bancaires (y compris Avoirs) = carte À payer,
par construction.

## REQUÊTES EN CLAIR (recoupement Robin)

À payer (carte = pied) :

SELECT COUNT(factures.id), COALESCE(SUM(COALESCE(factures.net_a_payer_ttc, factures.montant_ttc, depot_factures.montant_ttc, 0)), 0)
FROM factures
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)
WHERE factures.id IN (
  -- eligibleFacturesQuery : statut comptabilisee, WV paiement_effectif en_attente,
  -- pas de WV valide, latest depot comptabilisee AND saisie_historique = false
)
AND factures.deleted_at IS NULL;

À comptabiliser : DepotFacture::eligibleComptabilisation() (inchangé).

NV2 : deja_reglee = false AND saisie_historique = false AND normalized IN nv2_en_cours.

Banque : même socle à payer, GROUP BY compte_key (COALESCE override JSON, fe.intacct_id, défaut 512900/512901, 'non_rattache'), est_avoir, nature, origine, bucket.

## DÉPLOIEMENT-TEST

1. git pull origin main
2. find app/ config/ database/ -name "*.php" -exec touch {} +
3. php artisan optimize:clear && php artisan view:clear && php artisan route:clear
4. php artisan journal:sync

Pas de migrate. JAMAIS npm/composer/optimize/config:cache sur OVH.

Recoupement (les chiffres bougent dans la journée) :
- À comptabiliser : GS_COMPTABILITE → /factures/comptabilisation, compter les lignes = carte.
  DG → /factures?tab=tous&file_compta=1, même file, pas de 403.
- NV1 / NV2 : /factures?tab=tous&f_statut=nv1|nv2 = carte.
- En dépôt : onglet En dépôt = carte.
- À payer : nombre carte = pied bancaire. Montants TTC. Une ligne Avoirs si des avoirs.
- Banque : une seule carte « Non rattaché » ; plus aucun libellé snake_case.
- Ne pas comparer à une capture d’il y a plusieurs heures.

## LEÇON

Une clé de groupement doit produire le libellé, pas l’inverse. Deux
requêtes « à payer » finissent par diverger : une fonction, deux
consommateurs. Un clic vers un écran 403 pour un des trois groupes
n’est pas un tableau de bord. HT sur un décaissement est un défaut
métier, pas un détail d’affichage.
