---
suivi: 1218
date: 2026-08-05
sujet: diag pénalités × restant à facturer (double comptage avenants)
chantier: marches-frise-penalites
type: diagnostic
statut: poussé
hash: 9d0fc3c0
fichiers:
  - tools/diag/diag_penalites_restant_facturer_1218.php
  - docs/suivi/SUIVI_1218_diag_penalites_restant_facturer.md
---

## PROMPT ENVOYÉ

SUIVI #1218 PHASE 1 DIAGNOSTIC — NE PAS CODER. Pénalités acceptées × restant à
facturer : cartographie avant intégration à la frise. HEAD prod = `5b8dfc0f`.
Décisions Robin : avenant « PENALITES » = contournement ; pénalité ACCEPTÉE
impacte le RESTANT À FACTURER (pas le total marché) ; carte PÉNALITÉS = Σ acceptee
+ autres statuts hors calcul en gris.

## FICHES LUES

- `SUIVI_1191` — LEÇON : reprise données = décision Robin, pas data-fix Cursor ;
  retenues hors marché / mécanisme collecte historique à préserver.
- `SUIVI_1192` — grammaire UI (palette, pas d’empilement d’encadrés) pour objet 6.
- Aucune fiche antérieure dédiée « pénalités × frise » ; leçons #1204/#1205/#1211
  (consommateur hors calcul central) rappelées dans le prompt.

## SYNTHÈSE

Diagnostic lecture seule. Script OVH pour volumes prod. Code métier inchangé.

### OBJET 1 — Risque de double comptage

**Mécanisme** : `Marche::recalculer()` et la frise prennent `montant_total_ht` =
marché initial + avenants `signe`/`en_cours`. Un avenant négatif « PENALITES »
abaisse déjà le total → abaisse déjà le restant. Soustraire en plus Σ pénalités
`acceptee` du restant **sans supprimer l’avenant** = double comptage.

**Cas Robin (capture)** : avenant n°2 −149 065 € ≈ Σ 5 lignes acceptées 149 064 €
(écart arrondi 1 €) → **DOUBLE_COMPTAGE_PROBABLE** si on applique le correctif
frise sans bascule avenant.

**Volumes** : stub local ≠ prod. Lancer sur OVH :
`php tools/diag/diag_penalites_restant_facturer_1218.php`
Sections 3–4 listent avenants motif~PENALIT* et marchés acceptee sans avenant.

**Question à Robin (ne pas trancher)** : mise en œuvre frise **sans** reprise
préalable des avenants « PENALITES », ou **bascule coordonnée** (suppression /
neutralisation avenant + déduction restant) ? Chiffres OVH section 3 à coller.

### OBJET 2 — Inventaire « restant à facturer » / « reste à régler »

Deux notions distinctes (piège #1204-like) :

| Concept | Source | Formule | Pénalités aujourd’hui |
|---------|--------|---------|------------------------|
| **Restant à facturer (frise)** | `MarcheCouvertureKpiService::calculer` L124–125 | `totalHt − factureHt − provisionné(RG+finitions) − capté(CP+CIE+OPC)` | **Non soustraites** ; carte séparée |
| **Reste à régler HT (colonne)** | `Marche::recalculer` L282–288 | `montant_total_ht − facture_ht` | **Non** (sauf via avenant dans total) |

#### A. Pilotage / affichage (modification frise = décision produit Robin)

| Fichier:ligne | Rôle | Impact si Σ acceptee déduite du restant frise |
|---------------|------|-----------------------------------------------|
| `app/Services/MarcheCouvertureKpiService.php:125` | Formule restant frise | **Oui** — point d’entrée unique souhaitable |
| `app/Http/Controllers/MarcheController.php:1603–1648` | Enrichit couverture + cartes restant (origine / sous-traité) | Oui si service modifié |
| `app/Http/Controllers/MarcheController.php:1674–1684` | `penalites_ht` = Σ `notifiee`+`acceptee` | Cible Robin = **acceptee seule** pour le montant principal |
| `resources/js/Components/Marches/MarcheKpiHeader.vue:55,122–147,267–284` | Cartes FACTURÉ / RESTANT / PÉNALITÉS (`horsFrise: true`) | Oui affichage |
| `resources/js/Pages/Marches/Show.vue` (reste_a_regler* + onglet pénalités) | Affiche colonne + détail | Affichage ; colonne ≠ frise |
| `resources/js/Pages/Marches/Index.vue:166,508` | KPI / colonne « Reste » = `reste_a_regler_ht` | **Non** sauf si on change aussi `recalculer` |
| `resources/js/Pages/DepotFactures/DepotFactureSaisieForm.vue:2870–2885,9257` | Affiche `reste_a_regler_ht` marché | **Non** (colonne) |
| `app/Http/Controllers/DepotFactureController.php:3531,3702` | Payload reste marché | **Non** (colonne) |

#### B. Dashboard programme / bilan (proche pilotage agrégé — arbitrage si on touche `reste_a_regler_ht`)

| Fichier:ligne | Rôle | Formule / note |
|---------------|------|----------------|
| `app/Http/Controllers/ProgrammeDashboardController.php:102–105` | KPI Santé financière | Σ `reste_a_regler_ht` marches |
| `resources/js/Pages/Programmes/Dashboard.vue:171,440–494` | « reste à régler HT sur X engagés » | Affiche le KPI ci-dessus |
| `app/Services/BilanFinancierService.php:1131,1175` | `reste_ht` marches dans bilan | Lit `m.reste_a_regler_ht` — **pas** la frise |
| `app/Http/Controllers/MarcheController.php:2485–2502,2699` | Agrégats marches programme | Idem colonne |

Nomenclature `rubriques_bilan` / `postes_budgetaires` : pas de formule « restant à facturer frise » ; le bilan consomme les soldes marché/colonnes, pas `MarcheCouvertureKpiService`.

#### C. Comptable / Sage Intacct — signaler explicitement

| Fichier | Lien pénalités | Lien « restant à facturer » frise |
|---------|----------------|-----------------------------------|
| `app/Services/DepotFacture/EcrituresComptablesCalculator.php` (`ROLE_PENALITES`, L433,590) | Oui : lignes crédit pénalités TTC sur facture | **Non** — montants saisie dépôt |
| `ComptabilisationController` / flux APBILL | Pénalités **imputées** sur dépôt → écritures / Sage | **Non** — pas de champ restant frise |
| `IntacctService` / APBILL | Factures, pas solde marché | **Aucune** occurrence `reste_a_regler` / `restant_a_facturer` dans le flux Intacct inventorié |

**Conclusion inventaire** : modifier uniquement `MarcheCouvertureKpiService` + carte frise = **pilotage**. Modifier `Marche::recalculer` / `reste_a_regler_ht` = **dashboard + bilan** → arbitrage Sabrina BARTOLI + Rudy BENOR avant code. Les écritures Sage passent par l’**imputation** dépôt, pas par le restant frise — mais toute confusion des deux notions doit être évitée (#1204/#1205/#1211).

### OBJET 3 — Modèle des pénalités

- **Table** `penalites` (migration `2026_05_05_180500_create_penalites_table.php`) : FK `marche_id`, `programme_id` ; `type` enum ; `statut` enum ; `montant_ht`, `taux_tva`, `montant_ttc` (recalculé au save) ; `facture_id` nullable ; puis `depot_facture_id`, `date_imputation` (`2026_05_12_140000_…`) ; pivot `facture_penalites`.
- **Statuts stockés** : `notifiee`, `contestee`, `acceptee`, `deduite`, `annulee` (`PenaliteController::validatePayload` L106). **« Acceptée » = `acceptee`**.
- **Pas** de machine type `OffreStatutTransitions` (#1198) : enum libre à la validation.
- **Types** : `retard`, `non_conformite`, `sinistre`, `absence_reunion`, `sous_traitance_non_declaree`, `autre`.
- **HT/TVA/TTC** : HT + taux stockés ; TTC dérivé. Frise HT/TTC : alimenter l’affichage depuis HT (comme aujourd’hui `penalites_ht`) ; TTC = HT×(1+taux) si bascule TTC.
- **Audit** : pas de FieldAudit / historique statut dans `PenaliteController` — **trou** si passage en `acceptee` impacte un solde financier.
- **Droits** : routes sous groupe programmes/marchés (`routes/web.php` ~2065–2072) ; pas d’`authorize` dédié dans le contrôleur — accès = celui de la fiche marché.

### OBJET 4 — « Imputée sur »

- **Rôle** : rattacher une pénalité à un **dépôt facture** (`depot_facture_id` + `date_imputation` + pivot `facture_penalites`) pour la déduire du net à payer / alimenter les écritures (`EcrituresComptablesCalculator`).
- **UI** : `Marches/Show.vue` ~L4007 — vide si `depot_facture_id` null (cas Robin : 5 lignes acceptées non encore imputées).
- **Sélection dépôt** : `Marche::penalitesDisponiblesPourSelection` — `notifiee`|`acceptee` libres ou déjà sur ce dépôt.
- **Verdict** : mécanisme **VIVANT** (saisie dépôt, sync, release au rejet). Sur le marché capture : **non utilisé** (colonne vide), pas mort.
- **Question ouverte** : si déjà imputée sur une facture, faut-il encore la soustraire du restant frise (double matérialisation) ou seulement les non imputées ? À instruire avec Robin / compta — ne pas trancher.

### OBJET 5 — Cohérence retenues

Restant frise actuel retranche déjà :
- **Provisionné** : RG + finitions ;
- **Capté** : CP + CIE + OPC ;
- sous-lignes « à venir » (CP/OPC/CIE/RG) pour la part titulaire.

Les pénalités sont **parallèles** (carte `horsFrise`) et, à l’imputation, une **retenue de paiement** sur facture — sémantique Robin : acceptée = moins à facturer (pas une retenue de type RG). L’ajout frise ne doit pas toucher `EcrituresComptablesCalculator` retenues hors pénalités ni la collecte historique (#1191, #966+). Étendre éventuellement plus tard, sans casser RG/CIE/finitions.

### OBJET 6 — Affichage cible (écrit, non codé)

- Carte **PÉNALITÉS** : montant principal = Σ `acceptee` (HT ou TTC selon bascule).
- Sous-ligne gris : Σ autres statuts (ex. notifiée / contestée) + mention « hors calcul ».
- Carte **RESTANT À FACTURER** : solde **net** après déduction Σ acceptee ; sous-ligne « dont pénalités acceptées −X € » pour lever l’ambiguïté (évite un chiffre magique).
- Palette #1192 : rouge = danger uniquement. Proposition : pénalité **acceptée** = **fait acquis** (neutre / slate), pas danger ; danger réservé si Σ acceptee > restant avant déduction (solde négatif explicable).
- Si Σ acceptee > restant : **ne pas** afficher un négatif nu (leçon marché 32 RG) — message du type « pénalités acceptées dépassent le restant (écart Y €) » + solde plafonné à 0 **ou** solde négatif **avec** explication visible (choix Robin).

### OBJET 7 — Découpage Phase 2

| Lot | Contenu | Fichiers | Risque | Dépendance | Arbitrage |
|-----|---------|----------|--------|------------|-----------|
| **2a** | Décision data avenants PENALITES (liste OVH) | — (Robin) | Double comptage | Avant tout code frise sur marchés concernés | **Robin** |
| **2b** | Affichage frise seul : restant − Σ acceptee + carte PÉNALITÉS | `MarcheCouvertureKpiService.php`, `MarcheController.php` (penalites_ht), `MarcheKpiHeader.vue`, `public/build/` | Moyen UX | 2a si marchés à avenant | Robin (pilotage) ; **pas** Sabrina si on ne touche pas `reste_a_regler_ht` |
| **2c** | Aligner ou non `Marche::recalculer` / dashboard / bilan | `Marche.php`, `ProgrammeDashboardController`, `BilanFinancierService`, Dashboard.vue | **Élevé comptable** | Décision écrite | **Sabrina + Rudy** |
| **2d** | Imputation × restant (déjà facturée vs à venir) | Depot + KPI | Moyen | Clarification objet 4 | Robin + éventuellement compta |
| **2e** | Audit statut `acceptee` | FieldAudit / historique | Faible | 2b | Produit |

**Migration** : non nécessaire pour 2b (colonnes déjà là). Sérialisation : un seul lot à la fois sur `MarcheKpiHeader.vue` / `Show.vue` / `manifest.json`.

## DÉPLOIEMENT / TEST

1. `git pull origin main`
2. Sur OVH : `php tools/diag/diag_penalites_restant_facturer_1218.php`
3. Coller sections 3–4–5–6 dans le ticket ; décider 2a avant Phase 2 code.

## LEÇON

- Deux soldes : **restant frise** (retenues) ≠ **reste_a_regler_ht** (total − facturé). Toucher l’un n’alimente pas l’autre (#1204 pattern).
- Avenant « PENALITES » déjà dans le total : correctif restant sans bascule = double comptage.
- « Acceptée » = code `acceptee`. Carte actuelle = notifiee+acceptee.
- Imputation dépôt = vivant et chemin Sage ; colonne vide ≠ feature morte.
- Reprise avenants = décision Robin (#1191).
