diff --git a/generate_facturx_jinja2.py b/generate_facturx_jinja2.py
new file mode 100644
index 0000000..fd34e18
--- /dev/null
+++ b/generate_facturx_jinja2.py
@@ -0,0 +1,225 @@
+"""
+Générateur Factur-X BASIC via template Jinja2
+"""
+
+from jinja2 import Environment, FileSystemLoader
+from dataclasses import dataclass, field
+from typing import List, Optional
+from datetime import date
+from collections import defaultdict
+
+from fastapi.templating import Jinja2Templates
+
+templates = Jinja2Templates(directory="templates")
+
+# ─────────────────────────────────────────────
+# Structures de données (identiques au générateur lxml)
+# ─────────────────────────────────────────────
+@dataclass
+class Address:
+ line1: str
+ city: str
+ postcode: str
+ country_code: str
+ line2: Optional[str] = None
+
+
+@dataclass
+class Party:
+ name: str
+ vat_id: Optional[str] = None
+ siret: Optional[str] = None
+ address: Optional[Address] = None
+
+
+@dataclass
+class InvoiceLine:
+ line_id: str
+ description: str
+ quantity: float
+ unit_code: str
+ unit_price: float
+ vat_rate: float
+ vat_category: str = "S"
+
+# Raison d'exonération par catégorie TVA
+EXEMPTION_REASONS = {
+ "E": "TVA non applicable, art. 293 B du CGI",
+ "Z": "Taux zéro",
+ "AE": "Autoliquidation",
+ "K": "Exonération intracommunautaire",
+ "G": "Exportation hors UE",
+}
+
+@dataclass
+class TaxLine:
+ rate: float
+ category: str
+ base_amount: float
+ amount: float
+ exemption_reason: Optional[str] = None
+
+@dataclass
+class Invoice:
+ number: str
+ issue_date: date
+ currency: str
+ seller: Party
+ buyer: Party
+ lines: List[InvoiceLine] = field(default_factory=list)
+ due_date: Optional[date] = None
+ type_code: str = "380"
+ payment_means_code: str = "30"
+ iban: Optional[str] = None
+ payment_reference: Optional[str] = None
+ note: Optional[str] = None
+
+ # Calculés automatiquement (voir compute())
+ tax_lines: List[TaxLine] = field(default_factory=list)
+ total_ht: float = 0.0
+ total_tva: float = 0.0
+ total_ttc: float = 0.0
+
+ def compute(self):
+ """Calcule les totaux et les lignes de TVA depuis les lignes de facture."""
+ tax_groups = defaultdict(float)
+ self.total_ht = 0.0
+
+ for ln in self.lines:
+ net = round(ln.quantity * ln.unit_price, 2)
+ self.total_ht += net
+ tax_groups[(ln.vat_rate, ln.vat_category)] += net
+
+ self.total_ht = round(self.total_ht, 2)
+ self.tax_lines = []
+ self.total_tva = 0.0
+
+ for (rate, category), base in tax_groups.items():
+ base = round(base, 2)
+ amount = round(base * rate / 100, 2)
+ self.total_tva += amount
+ self.tax_lines.append(TaxLine(
+ rate=rate,
+ category=category,
+ base_amount=base,
+ amount=amount,
+ # Raison d'exonération automatique si catégorie != "S" (standard)
+ exemption_reason=EXEMPTION_REASONS.get(category) if category != "S" else None,
+ ))
+
+ self.total_tva = round(self.total_tva, 2)
+ self.total_ttc = round(self.total_ht + self.total_tva, 2)
+
+
+# ─────────────────────────────────────────────
+# Filtres Jinja2 personnalisés
+# ─────────────────────────────────────────────
+def filter_amount(value: float) -> str:
+ return f"{float(value):.2f}"
+
+
+def filter_datefmt(value: date) -> str:
+ return value.strftime("%Y%m%d")
+
+
+# ─────────────────────────────────────────────
+# Générateur
+# ─────────────────────────────────────────────
+def generate_facturx_xml(invoice: Invoice, template_dir: str = ".", output_file: str = "factur-x.xml") -> str:
+ """
+ Génère le XML Factur-X BASIC depuis le template Jinja2.
+ Retourne le XML sous forme de chaîne et l'écrit dans output_file.
+ """
+ # Calcul des totaux
+ invoice.compute()
+
+ # Environnement Jinja2
+ env = Environment(
+ loader=FileSystemLoader(template_dir),
+ autoescape=False, # XML géré manuellement
+ trim_blocks=True, # supprime le \n après les blocs {% %}
+ lstrip_blocks=True, # supprime les espaces avant {% %}
+ )
+ env.filters["amount"] = filter_amount
+ env.filters["datefmt"] = filter_datefmt
+
+ template = env.get_template("templates/xml/factur-x-basic.xml.j2")
+ xml_str = template.render(
+ invoice=invoice,
+ seller=invoice.seller,
+ buyer=invoice.buyer,
+ )
+
+ with open(output_file, "w", encoding="utf-8") as f:
+ f.write(xml_str)
+
+ return xml_str
+
+
+# ─────────────────────────────────────────────
+# Exemple d'utilisation
+# ─────────────────────────────────────────────
+if __name__ == "__main__":
+
+ invoice = Invoice(
+ number="FA-2024-00123",
+ issue_date=date(2024, 3, 15),
+ due_date=date(2024, 4, 14),
+ currency="EUR",
+ type_code="380",
+ note="Facture relative au contrat n°CTR-2024-01",
+
+ seller=Party(
+ name="Ma Société SAS",
+ # vat_id absent : pas de numéro TVA en franchise (art. 293 B CGI)
+ siret="12345678900012",
+ address=Address(
+ line1="10 rue de la Paix",
+ city="Paris",
+ postcode="75001",
+ country_code="FR",
+ ),
+ ),
+
+ buyer=Party(
+ name="Client SARL",
+ vat_id="FR98765432100",
+ siret="98765432100011",
+ address=Address(
+ line1="5 avenue des Fleurs",
+ city="Lyon",
+ postcode="69001",
+ country_code="FR",
+ ),
+ ),
+
+ payment_means_code="30",
+ iban="FR7612345678901234567890189",
+ payment_reference="CTR-2024-01",
+ )
+
+ # Ajout des lignes — franchise de TVA (art. 293 B du CGI)
+ # Utiliser vat_category="E" et vat_rate=0.0
+ articles = [
+ {"desc": "Développement logiciel - Sprint 1", "qty": 10, "unit": "HUR", "prix": 150.0, "tva": 0.0, "cat": "E"},
+ {"desc": "Licence logicielle annuelle", "qty": 1, "unit": "C62", "prix": 500.0, "tva": 0.0, "cat": "E"},
+ {"desc": "Formation utilisateurs", "qty": 2, "unit": "HUR", "prix": 200.0, "tva": 0.0, "cat": "E"},
+ ]
+
+ for i, art in enumerate(articles, start=1):
+ invoice.lines.append(InvoiceLine(
+ line_id=str(i),
+ description=art["desc"],
+ quantity=art["qty"],
+ unit_code=art["unit"],
+ unit_price=art["prix"],
+ vat_rate=art["tva"],
+ vat_category=art["cat"], # "E" = exonéré (franchise TVA)
+ ))
+
+ xml = generate_facturx_xml(invoice, template_dir=".", output_file="factur-x.xml")
+
+ print(f"✅ Fichier généré : factur-x.xml")
+ print(f" Total HT : {invoice.total_ht:.2f} €")
+ print(f" TVA 20% : {invoice.total_tva:.2f} €")
+ print(f" Total TTC : {invoice.total_ttc:.2f} €")
diff --git a/templates/xml/factur-x-basic.xml.j2 b/templates/xml/factur-x-basic.xml.j2
new file mode 100644
index 0000000..72bdea2
--- /dev/null
+++ b/templates/xml/factur-x-basic.xml.j2
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+ urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic
+
+
+
+
+
+ {{ invoice.number }}
+ {{ invoice.type_code }}
+
+ {{ invoice.issue_date | datefmt }}
+
+ {% if invoice.note %}
+
+ {{ invoice.note }}
+
+ {% endif %}
+
+
+
+
+
+
+ {% for line in invoice.lines %}
+
+
+ {{ line.line_id }}
+
+
+ {{ line.description }}
+
+
+
+ {{ line.unit_price | amount }}
+
+
+
+ {{ line.quantity | amount }}
+
+
+
+ VAT
+ {{ line.vat_category }}
+ {{ line.vat_rate | amount }}
+
+
+ {{ (line.quantity * line.unit_price) | amount }}
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {{ seller.name }}
+ {% if seller.siret %}
+
+ {{ seller.siret }}
+
+ {% endif %}
+ {% if seller.address %}
+
+ {{ seller.address.postcode }}
+ {{ seller.address.line1 }}
+ {% if seller.address.line2 %}
+ {{ seller.address.line2 }}
+ {% endif %}
+ {{ seller.address.city }}
+ {{ seller.address.country_code }}
+
+ {% endif %}
+ {% if seller.vat_id %}
+
+ {{ seller.vat_id }}
+
+ {% elif seller.siret %}
+ {# Franchise TVA (art. 293 B CGI) : pas de n° TVA, on utilise le SIRET avec schemeID="FC" #}
+
+ {{ seller.siret }}
+
+ {% endif %}
+
+
+
+ {{ buyer.name }}
+ {% if buyer.siret %}
+
+ {{ buyer.siret }}
+
+ {% endif %}
+ {% if buyer.address %}
+
+ {{ buyer.address.postcode }}
+ {{ buyer.address.line1 }}
+ {% if buyer.address.line2 %}
+ {{ buyer.address.line2 }}
+ {% endif %}
+ {{ buyer.address.city }}
+ {{ buyer.address.country_code }}
+
+ {% endif %}
+
+
+ {% if invoice.payment_reference %}
+
+ {{ invoice.payment_reference }}
+
+ {% endif %}
+
+
+
+
+
+
+ {{ buyer.name }}
+
+
+
+
+
+ {{ invoice.currency }}
+
+
+ {{ invoice.payment_means_code }}
+ {% if invoice.iban %}
+
+ {{ invoice.iban }}
+
+ {% endif %}
+
+
+ {% for tax in invoice.tax_lines %}
+
+ {{ tax.amount | amount }}
+ VAT
+ {% if tax.exemption_reason %}
+ {{ tax.exemption_reason }}
+ {% endif %}
+ {{ tax.base_amount | amount }}
+ {{ tax.category }}
+ {{ tax.rate | amount }}
+
+ {% endfor %}
+
+ {% if invoice.due_date %}
+
+
+ {{ invoice.due_date | datefmt }}
+
+
+ {% endif %}
+
+
+ {{ invoice.total_ht | amount }}
+ {{ invoice.total_ht | amount }}
+ {{ invoice.total_tva | amount }}
+ {{ invoice.total_ttc | amount }}
+ {{ invoice.total_ttc | amount }}
+
+
+
+
+
+
diff --git a/templates/xml/factur-x-basic.xml.j2.back b/templates/xml/factur-x-basic.xml.j2.back
new file mode 100644
index 0000000..654bb02
--- /dev/null
+++ b/templates/xml/factur-x-basic.xml.j2.back
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+ urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic
+
+
+
+
+
+ {{ invoice.number }}
+ {{ invoice.type_code }}
+
+ {{ invoice.issue_date | datefmt }}
+
+ {% if invoice.note %}
+
+ {{ invoice.note }}
+
+ {% endif %}
+
+
+
+
+
+
+ {% for line in invoice.lines %}
+
+
+ {{ line.line_id }}
+
+
+ {{ line.description }}
+
+
+
+ {{ line.unit_price | amount }}
+
+
+
+ {{ line.quantity | amount }}
+
+
+
+ VAT
+ {{ line.vat_category }}
+ {{ line.vat_rate | amount }}
+
+
+ {{ (line.quantity * line.unit_price) | amount }}
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {{ seller.name }}
+ {% if seller.siret %}
+
+ {{ seller.siret }}
+
+ {% endif %}
+ {% if seller.address %}
+
+ {{ seller.address.postcode }}
+ {{ seller.address.line1 }}
+ {% if seller.address.line2 %}
+ {{ seller.address.line2 }}
+ {% endif %}
+ {{ seller.address.city }}
+ {{ seller.address.country_code }}
+
+ {% endif %}
+ {% if seller.vat_id %}
+
+ {{ seller.vat_id }}
+
+ {% endif %}
+
+
+
+ {{ buyer.name }}
+ {% if buyer.siret %}
+
+ {{ buyer.siret }}
+
+ {% endif %}
+ {% if buyer.address %}
+
+ {{ buyer.address.postcode }}
+ {{ buyer.address.line1 }}
+ {% if buyer.address.line2 %}
+ {{ buyer.address.line2 }}
+ {% endif %}
+ {{ buyer.address.city }}
+ {{ buyer.address.country_code }}
+
+ {% endif %}
+
+
+ {% if invoice.payment_reference %}
+
+ {{ invoice.payment_reference }}
+
+ {% endif %}
+
+
+
+
+
+
+ {{ buyer.name }}
+
+
+
+
+
+ {{ invoice.currency }}
+
+
+ {{ invoice.payment_means_code }}
+ {% if invoice.iban %}
+
+ {{ invoice.iban }}
+
+ {% endif %}
+
+
+ {% for tax in invoice.tax_lines %}
+
+ {{ tax.amount | amount }}
+ VAT
+ {% if tax.exemption_reason %}
+ {{ tax.exemption_reason }}
+ {% endif %}
+ {{ tax.base_amount | amount }}
+ {{ tax.category }}
+ {{ tax.rate | amount }}
+
+
+
+
+ {{ tax.amount | amount }}
+ VAT
+ {{ tax.base_amount | amount }}
+ {{ tax.category }}
+ {{ tax.rate | amount }}
+
+ {% endfor %}
+
+ {% if invoice.due_date %}
+
+
+ {{ invoice.due_date | datefmt }}
+
+
+ {% endif %}
+
+
+ {{ invoice.total_ht | amount }}
+ {{ invoice.total_ht | amount }}
+ {{ invoice.total_tva | amount }}
+ {{ invoice.total_ttc | amount }}
+ {{ invoice.total_ttc | amount }}
+
+
+
+
+
+