# Rapport — v3.13.0 (factures : sections, éléments à la carte, numérotation assainie)

**Lot du socle : se livre aux DEUX installations (IRADA et RUWAD), à l'identique.**
**Deux migrations additives (aucune donnée modifiée). Aucune commande de reprise.
Aucune dépendance composer nouvelle.**

---

## A — Numérotation : la suggestion redevient la vraie suite

### Le symptôme

À la création d'une facture chez IRADA, la suggestion affichait **906-2026** alors que la vraie
suite était **16-2026**.

### La cause

La reprise a rangé les factures « bis » et sans numéro dans une **bande hors-série ≥ 900** de la
séquence technique (`03 bis-2025` → séquence 900, `05-2026 bis` → séquence 905), pour respecter
l'unicité (année, séquence) sans toucher aux numéros affichés. C'est **voulu** — mais
`InvoiceService::nextSequence()` prenait `max(sequence) + 1` sans exclure cette bande : la
suggestion partait de 905.

### Le correctif

- `nextSequence($year)` **ignore les séquences ≥ 900** (`InvoiceService::HORS_SERIE_MIN`). Avec
  {…, 15, 900, 905} en base sur l'année, la suggestion est **16**. La bande 900+ reste réservée
  aux « bis »/hors-série de la reprise — le lot n'y touche pas.
- Le **numéro est désormais visible et saisissable** sur le formulaire, suggéré automatiquement,
  et **modifiable tant que la facture n'est pas émise** (statut brouillon). Format `NN-AAAA`
  contrôlé, l'année du numéro doit être celle de la date, **unicité contrôlée** sur le numéro ET
  sur le couple (année, séquence) — y compris face aux séquences occupées par la bande hors-série —
  avec message clair en cas de doublon. À la création, changer l'année de la date re-suggère le
  numéro.
- **Interdits absolus respectés (art. 145 CGI)** : une facture émise garde son numéro (champ
  désactivé, non soumis, ignoré côté serveur) ; aucun numéro existant n'est modifié en base ;
  rien ne décrémente.

## B — Sections de lignes (sous-titres qui regroupent plusieurs lignes)

Certaines factures présentent leurs lignes **groupées sous des sous-titres** (« Phase 1 —
Diagnostic », « Livrables », « Frais »), avec **sous-total HT de section**.

### Modélisation retenue

Un champ **`section_title`** nullable **porté par les lignes** (`invoice_lines` et `quote_lines`),
plutôt qu'une table de sections séparée. C'est le choix le plus robuste avec le Repeater Filament
(pas de repeater imbriqué, pas de double glisser-déposer) et avec dompdf : **l'ordre des lignes
reste le seul maître**, les lignes consécutives qui portent le même titre forment une section.
Migration **additive** ; les factures existantes n'ont aucune section et leur PDF est inchangé.

### À l'édition

Chaque ligne du Repeater porte un champ facultatif **« Section (sous-titre) »**. Une facture sans
section s'édite exactement comme avant. L'ordre est maîtrisé par le glisser-déposer existant du
Repeater.

### Sur le PDF

- **Bandeau de section** sur toute la largeur (fond clair, filet or, texte navy en capitales —
  la charte du gabarit) ;
- lignes numérotées **en continu** à travers les sections ;
- **« Sous-total HT - {titre} »** en fin de chaque section (tiret ASCII : DejaVu ne rend pas le
  tiret cadratin, `PdfText` le translittère déjà partout ailleurs) ;
- totaux généraux inchangés ;
- multi-pages : `page-break-after: avoid` + `page-break-inside: avoid` sur le bandeau pour qu'il
  ne reste pas orphelin en bas de page (meilleur effort dompdf).

### Devis et bons de livraison

Les **devis** partagent le même modèle (`quote_lines.section_title`, même champ au formulaire,
même rendu PDF) et la conversion **devis → facture conserve les sections**. Les **bons de
livraison** n'ont **pas** de sections : un BL ne porte ni prix ni sous-totaux, et sa table de
lignes n'a pas la colonne — la conversion devis → BL les laisse simplement tomber. Si le besoin
apparaît un jour, c'est la même mécanique à répliquer.

## C — Les éléments « à la carte »

Chaque élément n'apparaît sur le PDF **que s'il est renseigné** ; une facture qui n'en utilise
aucun rend **exactement comme avant** (vérifié par 24 fixtures représentatives, deux variantes).
Tout se saisit dans une section **« Options d'affichage »**, repliée par défaut, du formulaire.

| Élément | Champ(s) | Rendu |
|---|---|---|
| Période de la prestation | `service_period_start` / `service_period_end` | « Période de la prestation : du … au … » dans le bloc référence |
| Date de livraison | `delivery_date` | « Date de livraison : … » dans le bloc référence |
| N° bon de commande / décompte | `order_reference` | dans le bloc référence, en plus de la réf. marché |
| Remise par ligne | `invoice_lines.discount_rate` / `discount_amount` | le total HT de la ligne devient net de remise |
| Remise globale | `global_discount_rate` / `global_discount_amount` | totaux : « TOTAL HT BRUT », « Remise », « TOTAL HORS TAXE (NET) » |
| Acompte déjà facturé | `advance_amount` | « Acompte déduit » + le net devient « RESTE À PAYER » |
| Retenue de garantie | `retention_rate` (montant figé `retention_amount`) | ligne dans le bloc totaux, déduite du net |
| Exonération de TVA | `vat_exemption_reason` | mention sous les totaux, régime « sans TVA » uniquement |
| Notes de pied | `footer_note` | petit paragraphe avant le pied de page |
| Colonne Unité | — | se masque si aucune ligne n'en a |

### La chaîne de calcul (au centime)

```
HT brut des lignes
  − remises de ligne (% OU montant, le montant prime)
  − remise globale (% OU montant, le montant prime)
= HT NET  → TVA → TTC
  − RAS TVA (% de la TVA) − RAS IS (% du HT net)
  − retenue de garantie (% du TTC)
  − acompte déjà facturé
= net à encaisser (« reste à payer »)
```

Deux points de doctrine :

- **`net_payable` devient le « reste à payer »** quand garantie et/ou acompte sont renseignés :
  c'est ce montant que l'encaissement (`markCollected`) inscrit en trésorerie — le cash réellement
  attendu sur cette facture. Sans ces éléments, rien ne change.
- **La variante acheteur (client) masque toujours les retenues à la source**, mais **affiche** les
  déductions contractuelles (remise, garantie, acompte) : son « reste à payer » repart du TTC,
  sans jamais révéler les RAS. Le montant en toutes lettres reste le TTC côté client, le net côté
  comptable — comme avant.

Les **notes internes** (`notes`, où la reprise a consigné ses mentions) ne sont **toujours pas
imprimées** : les notes de pied sont un champ distinct (`footer_note`).

## Recette (tout au vert)

- **Numérotation** : suggestion 16 avec {15, 900, 905} ; unicité (formulaire + service, y compris
  collision avec une séquence hors-série) ; numéros existants intouchés ; facture émise jamais
  renumérotée. — `InvoiceServiceTest`, `InvoiceResourceTest`
- **Sections** : 2 sections → bandeaux + sous-totaux au centime, numérotation continue, totaux
  généraux inchangés ; sans section → aucun marquage de section ; conversion devis → facture
  conserve les sections. — `InvoicePdfViewTest`, `QuoteServiceTest`
- **Éléments optionnels** : chacun apparaît si renseigné, disparaît sinon ; remise/acompte/
  garantie au centime (HT → TVA → TTC → RAS → net) ; variante client masque toujours les RAS ;
  exonération seulement en régime sans TVA ; colonne Unité masquée sans unités. —
  `InvoiceOptionsTest`, `InvoicePdfViewTest`
- **Non-régression** : 24 factures fixtures représentatives du parc réel (RAS TVA 0/75/100 ×
  RAS IS 0/5 × montants variés), rendues dans les deux variantes : aucun marquage nouveau,
  libellés et montants historiques intacts. — `InvoicePdfNonRegressionTest`
- **Suite complète verte** + `pint` + `npm run build`.

## Fichiers touchés

- `app/Services/Invoices/InvoiceService.php` — bande hors-série ignorée, `numberingFromNumber()`
- `app/Filament/Admin/Resources/Invoices/Schemas/InvoiceForm.php` — numéro, sections, options
- `app/Filament/Admin/Resources/Invoices/Pages/{CreateInvoice,EditInvoice}.php` — numéro manuel
- `app/Models/Invoice.php` — chaîne de calcul étendue (remise globale, garantie, acompte)
- `app/Models/InvoiceLine.php`, `app/Models/QuoteLine.php` — remise de ligne, section
- `app/Support/SectionsDeLignes.php` — regroupement des lignes en sections consécutives
- `app/Services/Quotes/QuoteService.php` — conversion conserve les sections
- `app/Filament/Admin/Resources/Quotes/Schemas/QuoteForm.php` — champ section
- `resources/views/pdf/invoice.blade.php`, `resources/views/pdf/quote.blade.php`
- `database/migrations/2026_08_22_100000_add_section_title_to_invoice_and_quote_lines.php`
- `database/migrations/2026_08_22_110000_add_display_options_to_invoices.php`
