Metadata-Version: 2.4
Name: ads-facturx
Version: 0.7.5
Summary: Génération, validation et assemblage de factures électroniques Factur-X / EN 16931 en Python.
License-Expression: MIT
Author: antoineducoulombier
Author-email: antoine.ducoulombier@alchimiedatasolutions.com
Requires-Python: >=3.10,<4
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Provides-Extra: dev
Requires-Dist: build (>=1.2,<2.0) ; extra == "dev"
Requires-Dist: factur-x (>=3.15,<4.0)
Requires-Dist: pikepdf (>=10.10.0,<11.0.0)
Requires-Dist: poetry (>=2.0,<3.0) ; extra == "dev"
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
Requires-Dist: pytest (>=8.3,<9.0) ; extra == "dev"
Requires-Dist: reportlab (>=4.4.9,<5.0.0)
Requires-Dist: ruff (>=0.16,<0.17) ; extra == "dev"
Requires-Dist: saxonche (>=12.9.0,<13.0.0)
Requires-Dist: twine (>=5.0,<7.0) ; extra == "dev"
Description-Content-Type: text/markdown

<h1 align="center">eFacturePy — <code>ads-facturx</code></h1>

<p align="center">
  <em>Génération, validation et assemblage de factures électroniques <b>Factur-X / EN 16931</b> en Python.</em>
</p>

<p align="center">
  <img alt="Python" src="https://img.shields.io/badge/python-%E2%89%A53.10-blue">
  <img alt="Version" src="https://img.shields.io/badge/ads--facturx-0.7.5-success">
  <img alt="Build" src="https://img.shields.io/badge/build-poetry-60A5FA">
  <img alt="Status" src="https://img.shields.io/badge/status-active-brightgreen">
</p>

---

## 🧭 Sommaire

- [À propos](#-à-propos)
- [Fonctionnalités](#-fonctionnalités)
- [Architecture du dépôt](#-architecture-du-dépôt)
- [Installation](#-installation)
- [Démarrage rapide](#-démarrage-rapide)
- [API publique](#-api-publique)
- [Utiliser une autre police](#-utiliser-une-autre-police)
- [Modèle de données](#-modèle-de-données)
- [Profils EU et FR](#-profils-eu-et-fr)
- [Documentation détaillée](#-documentation-détaillée)
- [Validation EN 16931](#-validation-en-16931)
- [Développement](#-développement)
- [Dépendances](#-dépendances)
- [Auteur](#-auteur)

---

## 📖 À propos

**eFacturePy** est le dépôt de travail du package Python **[`ads-facturx`](./ads_facturx)**, un outil qui permet de :

- produire un **XML Cross Industry Invoice** conforme au profil `urn:cen.eu:en16931:2017` ;
- générer le **PDF/A-3** associé à l’aide de [ReportLab](https://www.reportlab.com/) ;
- **embarquer** ce XML dans le PDF pour obtenir une **Factur-X** valide (via [`factur-x`](https://pypi.org/project/factur-x/)) ;
- **valider** la facture via XSD (EN16931) et **Schematron** (moteur [SaxonC-HE](https://pypi.org/project/saxonche/)).

---

## ✨ Fonctionnalités

- ✅ Génération d’un XML Factur-X / EN 16931 à partir d’un `dict` métier
- ✅ Deux profils : **EU** (EN16931) et **FR** (flux B2B français, BR-FR)
- ✅ Génération d’un PDF de facture (template par défaut + logo personnalisable)
- ✅ Fusion PDF + XML → fichier **PDF/A-3** conforme
- ✅ Validation **XSD**, **Schematron** (SaxonC) et **PDF/A-3** (veraPDF)
- ✅ Modèles **Pydantic v2** pour typer et valider les données métier
- ✅ Polices et profil ICC embarqués — rien à installer à côté

---

## 🏗️ Architecture du dépôt

```text
eFacturePy/
├─ ads_facturx/              # Package distribué sur PyPI
│  ├─ Builders/
│  │  ├─ xml/                # Sérialisation CII (mapper, policy, profils)
│  │  └─ pdf/                # Composants, styles, rendu, polices, ICC
│  ├─ Validators/            # XSD, Schematron (XSLT EN16931-CII), veraPDF
│  └─ models/                # Schémas Pydantic (DevisData, Invoice, Profile…)
├─ tests/                    # Tests unitaires pytest
├─ docs/                     # Documentation + exemples exécutables
├─ scripts/                  # Outillage (smoke test « comme PyPI »)
├─ archive/                  # Ressources brutes : XSD, XSL, Schematron, Saxon
├─ output/                   # Sorties de `main.py` (PDF/XML générés)
├─ main.py                   # Démonstration de bout en bout
├─ pyproject.toml
└─ poetry.lock
```

---

## 📦 Installation

Depuis PyPI :

```bash
pip install ads-facturx
```

> **Prérequis :** Python **≥ 3.10**. SaxonC-HE est installé automatiquement via la dépendance `saxonche`.
>
> **Optionnel :** [veraPDF](https://verapdf.org/) dans le `PATH`, requis
> uniquement par `validate_pdf_a3`. Le reste de la chaîne fonctionne sans.

---

## 🚀 Démarrage rapide

Pipeline complet : validation → XML → contrôles XSD/Schematron → PDF → ICC →
Factur-X → contrôle PDF/A-3.

```python
import os

from ads_facturx import (
    DevisData,
    Profile,
    add_icc,
    export_pdf,
    export_xml,
    generate_facturx,
    validate_pdf_a3,
    write_xml_from_string,
    xml_check_schematron,
    xml_check_xsd,
)

DESTINATION = "output"
PROFILE = Profile.EU  # ou Profile.FR pour le flux B2B français

# 1. Données métier (dict) → modèle validé, contrôlé selon le profil
data = {...}  # voir docs/pdf/examples/sample_data.py
validated = DevisData.model_validate(data, context={"profile": PROFILE})

invoice_name = validated.invoice.name
pdf_name = f"{invoice_name}.pdf"
pdf_path = os.path.join(DESTINATION, pdf_name)

# 2. XML EN 16931 + validations
xml_string = export_xml(validated, PROFILE)
xml_path = write_xml_from_string(
    xml_string=xml_string,
    destination_folder=DESTINATION,
    file_name=f"{invoice_name}.xml",
)
xml_check_xsd(xml_string, PROFILE)
print(xml_check_schematron(xml_path=xml_path))

# 3. PDF (template par défaut)
export_pdf(title=invoice_name, output_path=pdf_path, data=validated)

# 4. Profil ICC / OutputIntent — prérequis PDF/A, à faire AVANT la Factur-X.
#    pikepdf n'écrit pas en place : on passe par un fichier temporaire.
tmp = pdf_path + ".icc.pdf"
add_icc(pdf_path, tmp)
os.replace(tmp, pdf_path)

# 5. PDF + XML → Factur-X (c'est ici que le PDF devient PDF/A-3)
generate_facturx(file_name=pdf_name, destination_folder=DESTINATION, xml=xml_string)

# 6. Contrôle de conformité (nécessite veraPDF dans le PATH)
print("PDF/A-3 :", validate_pdf_a3(pdf_path))
```

> ⚠️ **Les étapes 4 et 5 ne sont pas optionnelles.** `export_pdf` seul produit
> un PDF ordinaire : c'est `add_icc` qui pose l'OutputIntent et
> `generate_facturx` qui écrit les métadonnées PDF/A-3. Valider avant elles
> renverra toujours « non conforme ».

> Un jeu de données complet, validable tel quel par `DevisData`, est fourni
> dans [`docs/pdf/examples/sample_data.py`](./docs/pdf/examples/sample_data.py).
> [`main.py`](./main.py) exécute cette chaîne de bout en bout.

---

## 🔌 API publique

Exposée via `from ads_facturx import ...` (cf. [`ads_facturx/__init__.py`](./ads_facturx/__init__.py)).

**Pipeline principal**

| Symbole                   | Rôle                                                                 |
|---------------------------|----------------------------------------------------------------------|
| `DevisData`               | Modèle Pydantic v2 d'entrée (validation EN 16931).                   |
| `Profile`                 | `EU` (EN16931) ou `FR` (flux B2B français, extensions BR-FR).        |
| `export_xml(data, profile)` | Construit la chaîne XML CII pour le profil demandé.                |
| `write_xml_from_string`   | Sérialise l'XML sur disque (`destination_folder`, `file_name`).      |
| `xml_check_xsd`           | Valide l'XML avec le XSD `facturx` au niveau dicté par le profil.    |
| `xml_check_schematron`    | Applique un Schematron via SaxonC (EN16931-CII, ou BR-FR).           |
| `export_pdf`              | Génère le PDF de facture (ReportLab — template par défaut ou custom). |
| `add_icc`                 | Ajoute le profil ICC / OutputIntent — prérequis PDF/A.               |
| `generate_facturx`        | Embarque le XML dans le PDF pour produire la Factur-X finale.        |
| `validate_pdf_a3`         | Contrôle la conformité PDF/A-3 via veraPDF.                          |
| `register_fonts`          | Déclare des polices tierces, embarquées dans le PDF.                 |

**Briques pour personnaliser le PDF** (détails dans la [doc PDF](./docs/pdf/README.md))

`Template` · `ParagraphComponent` · `SpacerComponent` · `PageBreakComponent` ·
`BoxComponent` · `TableComponent` · `KeepInFrameComponent` · `DiyComponent` ·
`Header` · `Footer` · `TextElement` · `CenteredText` · `ImageElement` ·
`HorizontalLine` · `Theme` · `TableStyles` · `FontName` · `BODY_FONT` ·
`BOLD_FONT` · `NonEmbeddableFontError` ·
`TableConfig` · `TableColumn` · `TableBox` · `ParagraphConfig` ·
`KeepInFrameConfig` · `FormatText` · `FormatDate` · `ExtraRow` ·
`ExtraRowCell` · `DocConfig` · `mm` · `A4` · `colors`.

---

## 🔤 Utiliser une autre police

La librairie embarque **DejaVu** (régulier + gras) et s'en sert par défaut.
Trois écritures équivalentes pour la désigner :

```python
from ads_facturx import BODY_FONT, BOLD_FONT, FontName, Theme

Theme(name="titre", fontName=FontName.DejaVuBold)  # enum, auto-complété
Theme(name="titre", fontName=BOLD_FONT)  # constante
Theme(name="titre", fontName="DejaVu-Bold")  # littéral
```

Ces noms s'emploient partout où une police est attendue : `Theme(fontName=...)`,
`TableStyles(body_font=..., header_font=...)`, `DocConfig(page_number_font=...)`,
`TextElement(font=...)` et `CenteredText(font=...)`.

Pour employer une police à soi, il suffit de la déclarer à
`register_fonts` — inutile d'importer ReportLab :

```python
from ads_facturx import Theme, export_pdf, register_fonts
from ads_facturx.Builders.pdf.config.config import DocConfig

register_fonts(extra={"Inter": "fonts/Inter-Regular.ttf"})

titre = Theme(name="titre", fontName="Inter", fontSize=14)
```

Le nom logique (`"Inter"`) se réutilise ensuite dans `Theme(fontName=...)`,
`TableStyles(body_font=..., header_font=...)` et
`DocConfig(page_number_font=...)`. Un fichier `.ttf` introuvable lève un
`FileNotFoundError` immédiatement, plutôt qu'au moment du rendu.

Trois points à connaître :

- **La police est embarquée en sous-ensemble dans le PDF**, ce qu'exige PDF/A-3.
  Un document rendu entièrement dans une police tierce reste conforme.
- **Le template par défaut reste en DejaVu.** Pour changer la police de tout le
  document, il faut fournir ses propres `Theme` / `TableStyles` via
  `export_pdf(structure=...)`. Les clés `header` et `footer` y sont facultatives.
- **Les 14 polices standard du PDF sont refusées** (voir ci-dessous).

### Pourquoi `Helvetica` & consorts sont refusées

`Theme(fontName="Helvetica")` lève désormais un `NonEmbeddableFontError`, de
même que `Times`, `Courier`, `Symbol`, `ZapfDingbats` et leurs variantes de
casse (`"Helvetica-bold"`). Ces 14 polices sont fournies par le lecteur de PDF,
donc **jamais écrites dans le fichier**. Deux conséquences :

- PDF/A l'interdit — *« the font programs for all fonts used for rendering
  within a conforming file shall be embedded »* (ISO 19005-3, clause
  6.2.11.4.1). veraPDF rejette le document, et la facture avec.
- Helvetica, Times et Courier sont des marques déposées : `ads-facturx` ne peut
  pas en distribuer les fichiers pour contourner le point précédent.

Le piège, c'est qu'un tel PDF s'ouvre et s'imprime normalement : sans ce
contrôle, rien n'échoue avant la validation de conformité — souvent chez le
destinataire. La substitution (`register_fonts(extra={"Helvetica": ...})`) est
refusée elle aussi : ReportLab l'ignore silencieusement dès qu'il a déjà
instancié la police d'origine, ce qui rendrait la conformité dépendante de
l'ordre des appels.

Le remède est toujours le même — une police libre déclarée à `register_fonts`,
ou les `DejaVu` livrées avec la librairie.

---

## 🧾 Modèle de données

Schémas **Pydantic v2** dans [`ads_facturx/models`](./ads_facturx/models) :
`DevisData` (racine), `Partner`, `Address`, `Invoice`, `InvoiceLine`,
`InvoiceSummary`, `LegalInformation`, plus les énumérations normatives
(`InvoiceType`, `BusinessProcess`, `CountryCode`, `CurrencyCode`, `UnitCode`,
`VatCategory`).

`DevisData.model_validate(data)` applique les contraintes EN 16931 critiques
(formats de dates `YYYYMMDD`, codes pays/devise/unité, type TVA, …).

Détail des champs et exemple JSON complet :
[`docs/pdf/03-input-data.md`](./docs/pdf/03-input-data.md).

---

## 🇫🇷 Profils EU et FR

Le profil se choisit **à la validation et à l'export**, pas dans les données :

```python
validated = DevisData.model_validate(data, context={"profile": Profile.FR})
xml_string = export_xml(validated, Profile.FR)
```

|                          | `Profile.EU`            | `Profile.FR`                            |
|--------------------------|-------------------------|------------------------------------------|
| URN déclaré              | `urn:cen.eu:en16931:2017` | `…:1p0:extended` (conformant BR-FR)     |
| Niveau XSD (`xsd_level`) | `en16931`               | `extended`                               |
| Schematron               | `EN16931-CII-validation.xslt` | `BR-FR-Flux2-Schematron-CII_V1.3.0.xslt` |
| Champs requis en plus    | —                       | `legal_information`, `seller_siren`, les 4 `xml_*_scheme` / `xml_*_endpoint_id`, `business_process` |
| Types de facture (BT-3)  | `380`, `381`, `389`     | + `386` (acompte), `503` (avoir d'acompte) |

Deux contrôles métier sont appliqués à la validation, avant toute génération :

- **UC-BT-3** — les codes acompte `386` / `503` sont refusés hors profil FR.
- **UC-BT-23** — une facture d'acompte n'admet que les cadres de facturation
  `B1, S1, M1, B2, S2, M2`.

Un champ requis manquant lève une `ValidationError` nommant précisément le
champ, et non une erreur Schematron obscure en fin de chaîne.

---

## 📚 Documentation détaillée

La génération PDF dispose de sa propre documentation, en 12 sections
progressives :

➡ **[`docs/pdf/README.md`](./docs/pdf/README.md)** — table des matières.

Au menu : quickstart, mental model, schéma `DevisData`, construction de
templates, catalogue de composants, personnalisation (style / config),
header & footer, composants DIY, défauts fournis, cookbook, erreurs
courantes, référence API.

Tous les exemples sont **exécutables** :

```bash
python docs/pdf/examples/run_examples.py
# → PDFs écrits dans docs/pdf/examples/out/
```

---

## ✅ Validation EN 16931

Trois contrôles indépendants, à trois niveaux :

| Niveau | Fonction | Ce qui est vérifié |
|--------|----------|--------------------|
| Données | `DevisData.model_validate(data, context=…)` | types, énumérations normatives, complétude du profil, BT-3 × BT-23 |
| XML | `xml_check_xsd(xml, profile)` | grammaire — `flavor="facturx"`, niveau dicté par le profil |
| XML | `xml_check_schematron(xml_path, xslt_path=…)` | règles métier — renvoie les `failed-assert` (id, location, message) |
| PDF | `validate_pdf_a3(pdf_path)` | conformité PDF/A-3 via veraPDF |

Les deux Schematron livrés sont dans
[`ads_facturx/Validators/xslt/`](./ads_facturx/Validators/xslt/) :
`EN16931-CII-validation.xslt` (EU, appliqué par défaut) et
`BR-FR-Flux2-Schematron-CII_V1.3.0.xslt` (FR). Le second se sélectionne via
`xslt_path=` — voir [`main.py`](./main.py).

---

## 🧪 Développement

```bash
pip install -e ".[dev]"     # pytest, ruff, poetry, build, twine
pytest                      # suite complète
ruff check . && ruff format .
python docs/pdf/examples/run_examples.py   # tous les exemples de la doc
python scripts/smoke_install.py            # test « comme si ça venait de PyPI »
```

Le dernier construit la roue, l'installe dans un venv vierge et exécute un
scénario complet depuis un dossier étranger au dépôt : c'est ce qui attrape
une ressource oubliée dans le packaging. Détails dans
[`docs/tester-comme-pypi.md`](./docs/tester-comme-pypi.md).

---

## 📚 Dépendances

`factur-x`, `saxonche`, `pydantic`, `reportlab`, `pikepdf` (voir
[`pyproject.toml`](./pyproject.toml) et [`poetry.lock`](./poetry.lock)).

Le package embarque ses propres ressources : polices **DejaVu**, profil ICC
**sRGB2014**, XSLT Schematron et logo. Aucun téléchargement ni chemin système
n'est requis à l'exécution.

## 👤 Auteur

**Antoine Ducoulombier** — [Alchimie Data Solutions](http://www.alchimiedatasolutions.com) · `antoine.ducoulombier@alchimiedatasolutions.com`

