# Facturation électronique — génération CII (D22B) pour smartcentraldeveloppement

Ce document décrit le mapping entre votre base de données (`helpers/Modele_helpers.php`, modèles Eloquent `t_facture`, `t_ligne_facture`, `t_client`, `t_societe`, etc.) et le format **CII (Cross Industry Invoice)**, schéma **D22B**, profil **EN 16931 🇫🇷**, utilisé pour la facturation électronique interentreprises en France (réforme AIFE, PDP agréées, 2026-2027).

Il accompagne deux classes PHP prêtes à l'emploi :

- `app/Services/generer_data_cii_par_facture_service.php` — extrait et met en forme les données d'une facture (`App\Models\facture`) selon les blocs sémantiques EN 16931 (BT-xxx / BG-xxx).
- `app/Services/generer_cii_xml_service.php` — sérialise ce tableau en XML CII D22B valide.

Ces deux classes suivent exactement le même schéma que les classes déjà présentes dans votre projet pour Peppol BIS3/UBL (`generer_data_peppol_par_facture_service.php` et `generer_peppol_bis3_xml_service.php`), afin de rester cohérentes avec vos conventions existantes.

## 1. Pourquoi CII et pas seulement Peppol/UBL ?

Votre projet dispose déjà d'un générateur **Peppol BIS 3.0 (UBL)**. **CII** est le second syntaxe autorisée par la norme européenne EN 16931 (avec UBL) et c'est celle historiquement utilisée par **Factur-X/ZUGFeRD** (facture PDF + XML) et par le flux « natif » CII accepté par les plateformes de dématérialisation partenaires (PDP) françaises. Le référentiel externe de l'AIFE retient désormais le schéma **D22B** (au lieu de D16B) pour permettre, entre autres, plusieurs références à des factures d'origine sur un avoir (bloc BG-3, passé de cardinalité 0..1 à 0..n).

Les URN de namespace **n'ont pas changé** entre D16B et D22B :

| Préfixe | URI |
|---|---|
| `rsm` | `urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100` |
| `ram` | `urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100` |
| `qdt` | `urn:un:unece:uncefact:data:standard:QualifiedDataType:100` |
| `udt` | `urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100` |

Seul le contenu du schéma XSD change (nouveaux éléments optionnels, cardinalités assouplies). Un document D16B valide reste valide en D22B ; l'inverse n'est pas garanti.

## 2. Structure générale du document

```
rsm:CrossIndustryInvoice
├── rsm:ExchangedDocumentContext
│   └── ram:GuidelineSpecifiedDocumentContextParameter/ram:ID      (BT-24)
├── rsm:ExchangedDocument
│   ├── ram:ID                                                     (BT-1)
│   ├── ram:TypeCode                                                (BT-3)
│   ├── ram:IssueDateTime/udt:DateTimeString[@format=102]           (BT-2)
│   └── ram:IncludedNote/ram:Content                                (BT-22, répétable)
└── rsm:SupplyChainTradeTransaction
    ├── ram:IncludedSupplyChainTradeLineItem  (répété, une par ligne — BG-25)
    ├── ram:ApplicableHeaderTradeAgreement     (BG-4 vendeur / BG-7 acheteur)
    ├── ram:ApplicableHeaderTradeDelivery      (BG-13 livraison)
    └── ram:ApplicableHeaderTradeSettlement    (BG-16/19/21/22/23 paiement, TVA, totaux)
```

**Important** : dans le schéma CII, les lignes de facture (`IncludedSupplyChainTradeLineItem`) se placent **avant** les blocs d'en-tête (`ApplicableHeaderTradeAgreement`, etc.), contrairement à UBL où elles sont en fin de document. C'est l'ordre respecté par `generer_cii_xml_service.php`.

Toutes les dates utilisent le format imposé `udt:DateTimeString[@format="102"]`, soit `CCYYMMDD` (ex : `20260930` pour le 30 septembre 2026).

## 3. Dictionnaire des balises et correspondance avec vos tables

### 3.1 En-tête de facture

| BT | Balise XML | Table / champ source | Remarque |
|---|---|---|---|
| BT-1 | `rsm:ExchangedDocument/ram:ID` | `t_facture.facture_numero` | Numéro de facture |
| BT-2 | `rsm:ExchangedDocument/ram:IssueDateTime` | `t_facture.facture_date` | Format `CCYYMMDD` |
| BT-3 | `rsm:ExchangedDocument/ram:TypeCode` | dérivé de `t_facture.facture_avoir` | `380` = facture, `381` = avoir (UNTDID 1001) |
| BT-5 | `.../ram:InvoiceCurrencyCode` | fixe `EUR` | Pas de colonne devise identifiée sur `t_facture` |
| BT-9 | `.../ram:DueDateDateTime` | **à brancher** | Aucune date d'échéance stockée telle quelle sur `t_facture` ; à calculer à partir de `mode_reg_id` / `echeance_id` |
| BT-20 | `.../ram:Description` (PaymentTerms) | `t_mode_reg.mode_reg_libelle` | Libellé des conditions de paiement |
| BT-22 | `rsm:ExchangedDocument/ram:IncludedNote` | `t_facture.facture_commentaire_permanant_1/2` | Note libre |
| BT-24 | `.../ram:GuidelineSpecifiedDocumentContextParameter/ram:ID` | constante `guideline_id` | **À valider avec l'identifiant exact exigé par votre PDP** (spécification externe AIFE, profil « EN 16931 🇫🇷 ») |

### 3.2 Vendeur — BG-4 (`t_societe`)

| BT | Balise XML | Champ `t_societe` |
|---|---|---|
| BT-27 | `SellerTradeParty/ram:Name` | `Societe_Nom_Societe` |
| BT-29 | `SellerTradeParty/ram:GlobalID[@schemeID=0009]` | `Societe_SIRET` (SIRET, schéma ISO 6523 France) |
| BT-30 | `SpecifiedLegalOrganization/ram:ID[@schemeID=0002]` | `Societe_SIREN` |
| BT-31 | `SpecifiedTaxRegistration/ram:ID[@schemeID=VA]` | `Societe_Intracommunautaire` |
| BT-35 | `PostalTradeAddress/ram:LineOne` | `Societe_Adresse1` |
| BT-36 | `PostalTradeAddress/ram:LineTwo` | `Societe_Adresse2` |
| BT-37 | `PostalTradeAddress/ram:CityName` | `Societe_Ville` |
| BT-38 | `PostalTradeAddress/ram:PostcodeCode` | `Societe_Code_Postal` |
| BT-40 | `PostalTradeAddress/ram:CountryID` | `Societe_Pays`, normalisé (voir encadré ci-dessous) |
| BT-42 | `DefinedTradeContact/.../CompleteNumber` | `Societe_Telephone` |
| BT-43 | `DefinedTradeContact/.../URIID` | `Societe_Email` |
| — | `rsm:ExchangedDocument/ram:IncludedNote` | `Societe_Capital`, `Societe_RCS`, `Societe_APE` | Mentions légales françaises hors modèle EN16931 mais obligatoires au titre du code de commerce ; ajoutées en note libre. |

> **Codes pays (BT-40 / BT-55 / BT-80) :** ces balises exigent un code ISO 3166-1 alpha-2 (`FR`, `BE`...). Ni `Societe_Pays` ni `t_facture.facture_pays` ne sont fiables tels quels — un test réel a montré que `facture_pays` contient parfois un **code interne** (`01`) au lieu d'un code ISO, ce qui aurait produit un `CountryID` invalide. `generer_data_cii_par_facture_service.php` ne retient donc une valeur que si elle correspond au motif `^[A-Z]{2}$` (`normaliserPays()`), et retombe sinon sur `t_commune → t_pays.pays_code`, puis sur `FR` par défaut.

Le compte bancaire à afficher (IBAN/BIC) est choisi via `t_facture.banque_facture_id → t_banque` (et non un compte unique par société), ce qui permet de gérer plusieurs comptes bancaires par entité.

| BT | Balise XML | Champ `t_banque` |
|---|---|---|
| BT-84 | `PayeePartyCreditorFinancialAccount/ram:IBANID` | `banque_iban` |
| BT-85 | `.../ram:AccountName` | `banque_libelle1` |
| BT-86 | `PayeeSpecifiedCreditorFinancialInstitution/ram:BICID` | `banque_bic` |

### 3.3 Acheteur — BG-7

Le modèle `t_facture` fige, au moment de la facturation, un **instantané** des coordonnées du client (`facture_nom_facturation`, `facture_client_siret`, `facture_client_intracom`, `facture_adresse1/2/3`, `facture_code_postal`, `facture_ville`, `facture_pays`, `facture_client_mail_facturation`). C'est ce jeu de champs qui doit être utilisé en priorité (le client courant `t_client` peut avoir changé d'adresse depuis l'émission), avec repli sur `t_client` si l'instantané est vide (facture historique par exemple).

| BT | Balise XML | Champ prioritaire (`t_facture`) | Repli (`t_client`) |
|---|---|---|---|
| BT-44 | `BuyerTradeParty/ram:Name` | `facture_nom_facturation` | `client_nom` |
| BT-46 | `BuyerTradeParty/ram:ID` | — | `client_code` |
| BT-47 | `SpecifiedLegalOrganization/ram:ID[@schemeID=0009]` | `facture_client_siret` | `client_siret` |
| BT-48 | `SpecifiedTaxRegistration/ram:ID[@schemeID=VA]` | `facture_client_intracom` | `client_intracom` |
| BT-50 | `PostalTradeAddress/ram:LineOne` | `facture_adresse1` | `client_adresse1` |
| BT-51 | `PostalTradeAddress/ram:LineTwo` | `facture_adresse2` | `client_adresse2` |
| BT-52 | `PostalTradeAddress/ram:CityName` | `facture_ville` | `client_ville` |
| BT-53 | `PostalTradeAddress/ram:PostcodeCode` | `facture_code_postal` | `client_code_postal` |
| BT-55 | `PostalTradeAddress/ram:CountryID` | `facture_pays` | `t_commune.lier_pays.pays_code` |
| — | `DefinedTradeContact/.../URIID` | `facture_client_mail_facturation` | `client_mail_fac` |

> Astuce : si vous voulez distinguer BT-47 (SIRET établissement) et un futur BT-48-bis (SIREN entreprise) côté acheteur comme c'est fait côté vendeur, le SIREN se déduit du SIRET par troncature : `substr($siret, 0, 9)` (SIRET = SIREN 9 chiffres + NIC 5 chiffres).

### 3.4 Livraison — BG-13

Renseignée à partir du chantier (`t_chantier`) associé aux lignes de la facture (`t_ligne_facture.chantier_id`), pertinent pour une activité de livraison de béton prêt à l'emploi où le lieu de livraison diffère systématiquement de l'adresse de facturation du client.

| BT | Balise XML | Champ `t_chantier` |
|---|---|---|
| BT-70 | `ShipToTradeParty/ram:Name` | `chantier_nom` |
| BT-75 | `PostalTradeAddress/ram:LineOne` | `chantier_adresse1` (nom exact du champ à vérifier dans votre table) |
| BT-78 | `PostalTradeAddress/ram:CityName` | `chantier_ville` |
| BT-77 | `PostalTradeAddress/ram:PostcodeCode` | `chantier_code_postal` |
| BT-80 | `PostalTradeAddress/ram:CountryID` | via `t_commune.lier_pays.pays_code` |

### 3.5 Paiement — BG-16 / BG-19 / BG-21

| BT | Balise XML | Source |
|---|---|---|
| BT-81 | `SpecifiedTradeSettlementPaymentMeans/ram:TypeCode` | `t_mode_reg.mode_reg_code` → table de correspondance UNTDID 4461 (voir §4) |
| BT-84/85/86 | virement, cf. §3.2 | `t_banque` via `t_facture.banque_facture_id` |
| BT-89 | `ram:ID[@schemeID=SEPA]` (mandat) | `t_client.mandat` (si prélèvement) |
| BT-91 | `PayerPartyDebtorFinancialAccount/ram:IBANID` | `t_client.iban` (si prélèvement) |

### 3.6 Ventilation de la TVA — BG-23 (répété par taux/catégorie)

Calculée en regroupant les lignes de `t_ligne_facture` par `ligne_facture_tva_taux`.

| BT | Balise XML |
|---|---|
| BT-116 | `ApplicableTradeTax/ram:BasisAmount` (base HT du groupe) |
| BT-117 | `ApplicableTradeTax/ram:CalculatedAmount` (montant de TVA du groupe) |
| BT-118 | `ApplicableTradeTax/ram:CategoryCode` (S, Z, E, AE, K, G, O — UNTDID 5305) |
| BT-119 | `ApplicableTradeTax/ram:RateApplicablePercent` |
| BT-120/121 | `ExemptionReason` / `ExemptionReasonCode` (uniquement si catégorie ≠ S) |

⚠️ **Autoliquidation BTP** : votre secteur (fourniture de béton à des chantiers) est régulièrement concerné par l'autoliquidation de la TVA en sous-traitance (article 283-2 nonies du CGI, catégorie `AE`). Le taux stocké en base (`ligne_facture_tva_taux`) ne permet pas à lui seul de distinguer une ligne à 0 % « exonérée » d'une ligne à 0 % « autoliquidée » : cette information doit venir d'un indicateur métier explicite (à ajouter si absent) plutôt que d'être déduite du seul taux.

### 3.7 Totaux — BG-22

| BT | Balise XML | Source |
|---|---|---|
| BT-106 | `SpecifiedTradeSettlementHeaderMonetarySummation/ram:LineTotalAmount` | somme des lignes |
| BT-109 | `.../ram:TaxBasisTotalAmount` | `t_facture.facture_total_ht` |
| BT-110 | `.../ram:TaxTotalAmount` | `t_facture.facture_total_tva` |
| BT-112 | `.../ram:GrandTotalAmount` | HT + TVA |
| BT-115 | `.../ram:DuePayableAmount` | idem (pas d'acompte géré dans cette version) |

### 3.8 Lignes de facture — BG-25 (`t_ligne_facture`)

| BT | Balise XML | Champ `t_ligne_facture` |
|---|---|---|
| BT-126 | `AssociatedDocumentLineDocument/ram:LineID` | `ligne_facture_num_ligne` |
| BT-153 | `SpecifiedTradeProduct/ram:Name` | `ligne_facture_libelle_modifie` ou `ligne_facture_libelle_initial` |
| BT-155 | `SpecifiedTradeProduct/ram:SellerAssignedID` | `t_produit.produit_code` (via `article_id`) |
| BT-129/130 | `SpecifiedLineTradeDelivery/ram:BilledQuantity[@unitCode]` | `ligne_facture_qte_reelle` + unité (voir §4) |
| BT-146 | `NetPriceProductTradePrice/ram:ChargeAmount` | `ligne_facture_prix_unit_ht` |
| BT-131 | `SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount` | `ligne_facture_prix_total_ht` |
| BT-151/152 | `ApplicableTradeTax/ram:CategoryCode` / `RateApplicablePercent` | `ligne_facture_tva_taux` |

**Correction (test réel du 22/08) :** la première version de ce filtre excluait `['C','B','E']` (repris de `generer_data_peppol_par_facture_service.php`), mais un test sur facture réelle a montré qu'une ligne d'affichage type « Sous total chantier » passait au travers — montant HT non nul, mais pas un vrai poste facturable — ce qui **doublait le total HT** dans le XML (291,33 € au lieu de 156,43 €). Le filtre est donc inversé en liste blanche, reprenant **exactement** les types utilisés par `facture_controller::recalculerTotaux()` pour calculer `facture_total_ht`/`facture_total_tva` :

```php
protected array $lineTypesFacturables = ['F', 'S', 'J', 'D', 'Z', 'G'];
...
->whereIn('ligne_facture_type', $this->lineTypesFacturables)
```

Cela garantit que le total du XML correspond toujours au total officiel déjà calculé par l'application.

## 4. Tables de correspondance à valider / compléter

Ces deux mappings sont des **valeurs par défaut plausibles** posées dans `generer_data_cii_par_facture_service.php` (propriétés `$paymentMeansMap` et `$unitCodeMap`) : à ajuster aux codes réellement utilisés dans vos tables `t_mode_reg` et `t_unite`.

**Moyens de paiement (BT-81, UNTDID 4461)**

| `mode_reg_code` (exemple) | Code UNTDID 4461 | Libellé |
|---|---|---|
| VIR | 30 | Virement |
| PRLV | 49 | Prélèvement SEPA |
| CHQ | 20 | Chèque |
| CB | 48 | Carte bancaire |
| ESP | 10 | Espèces |

**Unités (BT-130, UN/CEFACT Recommandation n°20)**

| Unité interne (exemple) | Code Rec. 20 | Signification |
|---|---|---|
| M3 | MTQ | mètre cube (béton) |
| T | TNE | tonne |
| KG | KGM | kilogramme |
| U | C62 | unité |
| H | HUR | heure |
| KM | KMT | kilomètre |
| FORFAIT | LS | forfait |

## 5. Utilisation

```php
use App\Models\facture;
use App\Services\generer_data_cii_par_facture_service;
use App\Services\generer_cii_xml_service;

$facture = facture::with(['lier_societe', 'lier_client', 'lier_ligne_facture', 'lier_mode_reglement'])
    ->where('facture_numero', $numero)
    ->firstOrFail();

$data = (new generer_data_cii_par_facture_service())->generer_from_facture($facture);
$xml  = (new generer_cii_xml_service())->generer($data);

file_put_contents(storage_path("app/factures_electroniques/{$facture->facture_numero}.xml"), $xml);
```

## 6. Validation des champs obligatoires (fail-fast)

Avant de sérialiser le XML, `generer_from_facture()` vérifie que les champs rendus obligatoires par EN 16931 sont bien renseignés (nom/SIRET/n° de TVA du vendeur, nom de l'acheteur, numéro et date de facture, au moins une ligne facturable). S'il en manque un, la méthode lève une `\RuntimeException` avec un message explicite (ex : *« numéro de TVA intracommunautaire du vendeur (t_societe.Societe_Intracommunautaire) »*) plutôt que de produire un XML incomplet qui serait rejeté par la plateforme. `facture_controller::test_facture_electronique` capture cette exception et répond en HTTP 422 avec le message.

C'est ce qui explique l'erreur *« Numero de TVA du vendeur manquant »* remontée par le validateur externe sur la facture `F0426080037` : il ne s'agit pas d'un bug du générateur mais d'une donnée absente en base — le champ `Societe_Intracommunautaire` de la société **NOUVELLE CARRIERE ARCEY** (`t_societe`) est vide et doit être complété.

## 7. Avant mise en production

1. **Renseigner le numéro de TVA intracommunautaire de chaque société** (`t_societe.Societe_Intracommunautaire`) — obligatoire (BT-31), et actuellement bloquant tant qu'il est vide (cf. §6).
2. **Valider avec le Schematron officiel** du profil « EN 16931 🇫🇷 » (spécifications externes B2B publiées par l'AIFE / DGFiP, `impots.gouv.fr/specifications-externes-b2b`), pas seulement la conformité XML/XSD.
3. Confirmer la valeur exacte attendue pour `GuidelineSpecifiedDocumentContextParameter/ID` (BT-24) auprès de votre PDP.
4. Brancher le calcul réel de la date d'échéance (BT-9) — actuellement un point ouvert, aucun champ `t_facture` ne la stocke directement.
5. Compléter la détection de l'autoliquidation BTP (catégorie `AE`) par un indicateur métier explicite plutôt qu'une déduction depuis le seul taux de TVA.
6. Sur les avoirs (`facture_avoir`), relier explicitement chaque avoir à sa ou ses facture(s) d'origine pour alimenter BG-3 (`InvoiceReferencedDocument`, répétable en D22B).
7. Ajuster `$paymentMeansMap` et `$unitCodeMap` aux valeurs réelles de vos tables `t_mode_reg` et `t_unite`.
8. Revérifier `t_client.client_siret` : sur la facture testée, ce champ contenait `FR39745397800025` (préfixe `FR` + 14 chiffres), qui n'a pas la forme d'un SIRET valide (14 chiffres, sans préfixe pays) — probablement une confusion avec le numéro de TVA intracommunautaire lors de la saisie.

## Sources

- [Spécifications externes et normes pour la facturation électronique — impots.gouv.fr](https://www.impots.gouv.fr/specifications-externes-b2b)
- [Formats et profils de facture électronique du socle minimum — XP Z12-012 (E-Invoicing France)](https://www.e-invoicing-france.eu/documentation/xp-z12-012/AFNOR-FE-XP-Z12-012-4-Formats-et-profils)
- [Facturation électronique interentreprises — AIFE](https://aife.economie.gouv.fr/nos-applications/facturation-electronique-b2b/)
- [Factur-X 1.08 / ZUGFeRD 2.4 : nouveautés et migration — FacturX API](https://facturxapi.com/blog/facturx-108-zugferd-24-nouveautes-developpeurs)
- [Cross Industry Invoice - D22B — UNECE](https://unece.org/trade/documents/2023/06/standards/cross-industry-invoice-d22b)
