diff --git a/generate_facturx_basic.py b/generate_facturx_basic.py
deleted file mode 100644
index 7d78512..0000000
--- a/generate_facturx_basic.py
+++ /dev/null
@@ -1,345 +0,0 @@
-"""
-Usage
-pip install lxml
-python generate_facturx_basic.py
-"""
-
-"""
-Générateur de XML Factur-X - Profil BASIC
-Conforme à la norme EN 16931, syntaxe CII (Cross Industry Invoice)
-Factur-X 1.0 / ZUGFeRD 2.x
-"""
-
-from lxml import etree
-from datetime import date
-from dataclasses import dataclass, field
-from typing import List, Optional
-
-
-# ─────────────────────────────────────────────
-# Namespaces CII obligatoires
-# ─────────────────────────────────────────────
-NSMAP = {
- "rsm": "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100",
- "ram": "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100",
- "udt": "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100",
- "qdt": "urn:un:unece:uncefact:data:standard:QualifiedDataType:100",
- "xsi": "http://www.w3.org/2001/XMLSchema-instance",
-}
-RSM = NSMAP["rsm"]
-RAM = NSMAP["ram"]
-UDT = NSMAP["udt"]
-
-PROFILE_BASIC = "urn:factur-x.eu:1p0:basic"
-
-
-# ─────────────────────────────────────────────
-# Structures de données
-# ─────────────────────────────────────────────
-@dataclass
-class Address:
- line1: str
- city: str
- postcode: str
- country_code: str # ISO 3166-1 alpha-2, ex: "FR"
- line2: Optional[str] = None
-
-
-@dataclass
-class Party:
- name: str
- vat_id: Optional[str] = None # ex: "FR12345678901"
- siret: Optional[str] = None # schemeID="0002"
- address: Optional[Address] = None
-
-
-@dataclass
-class TaxLine:
- base_amount: float
- rate: float # ex: 20.0 pour 20%
- amount: float
- category: str = "E" # S=standard, Z=zéro, E=exonéré, AE=autoliquidation
-
-
-@dataclass
-class InvoiceLine:
- line_id: str
- description: str
- quantity: float
- unit_code: str # ex: "C62"=unité, "HUR"=heure, "KGM"=kg
- unit_price: float # prix HT unitaire
- vat_rate: float # ex: 20.0
- vat_category: str = "S"
- # Le montant HT de la ligne est calculé automatiquement
-
-
-@dataclass
-class Invoice:
- # En-tête
- number: str
- issue_date: date
- due_date: Optional[date]
- currency: str # ex: "EUR"
- type_code: str = "380" # 380=facture, 381=avoir, 384=facture rectificative
-
- # Parties
- seller: Party = None
- buyer: Party = None
-
- # Lignes
- lines: List[InvoiceLine] = field(default_factory=list)
-
- # Paiement
- payment_means_code: str = "30" # 30=virement
- iban: Optional[str] = None
- bic: Optional[str] = None
- payment_reference: Optional[str] = None
-
- # Note (champ libre)
- note: Optional[str] = None
-
-
-# ─────────────────────────────────────────────
-# Helpers XML
-# ─────────────────────────────────────────────
-def sub(parent, ns, tag, text=None, **attribs):
- el = etree.SubElement(parent, f"{{{ns}}}{tag}", **attribs)
- if text is not None:
- el.text = str(text)
- return el
-
-
-def date_str(d: date) -> str:
- return d.strftime("%Y%m%d")
-
-
-def amount_str(v: float) -> str:
- return f"{v:.2f}"
-
-
-# ─────────────────────────────────────────────
-# Construction du XML
-# ─────────────────────────────────────────────
-def build_facturx(inv: Invoice) -> etree._ElementTree:
-
- root = etree.Element(f"{{{RSM}}}CrossIndustryInvoice", nsmap=NSMAP)
-
- # ── 1. ExchangedDocumentContext ──────────────
- ctx = sub(root, RSM, "ExchangedDocumentContext")
- gp = sub(ctx, RAM, "GuidelineSpecifiedDocumentContextParameter")
- sub(gp, RAM, "ID", PROFILE_BASIC)
-
- # ── 2. ExchangedDocument ────────────────────
- doc = sub(root, RSM, "ExchangedDocument")
- sub(doc, RAM, "ID", inv.number)
- sub(doc, RAM, "TypeCode", inv.type_code)
- idt = sub(doc, RAM, "IssueDateTime")
- sub(idt, UDT, "DateTimeString", date_str(inv.issue_date), format="102")
- if inv.note:
- sub(doc, RAM, "IncludedNote").append(
- etree.fromstring(f'{inv.note}')
- )
-
- # ── 3. SupplyChainTradeTransaction ──────────
- tx = sub(root, RSM, "SupplyChainTradeTransaction")
-
- # ── 3a. Lignes de facture (BASIC = lignes obligatoires) ──
- for ln in inv.lines:
- net = round(ln.quantity * ln.unit_price, 2)
- ili = sub(tx, RAM, "IncludedSupplyChainTradeLineItem")
- assoc = sub(ili, RAM, "AssociatedDocumentLineDocument")
- sub(assoc, RAM, "LineID", ln.line_id)
-
- prod = sub(ili, RAM, "SpecifiedTradeProduct")
- sub(prod, RAM, "Name", ln.description)
-
- la = sub(ili, RAM, "SpecifiedLineTradeAgreement")
- gpp = sub(la, RAM, "GrossPriceProductTradePrice")
- sub(gpp, RAM, "ChargeAmount", amount_str(ln.unit_price), currencyID=inv.currency)
- npp = sub(la, RAM, "NetPriceProductTradePrice")
- sub(npp, RAM, "ChargeAmount", amount_str(ln.unit_price), currencyID=inv.currency)
-
- ld = sub(ili, RAM, "SpecifiedLineTradeDelivery")
- sub(ld, RAM, "BilledQuantity", amount_str(ln.quantity), unitCode=ln.unit_code)
-
- ls = sub(ili, RAM, "SpecifiedLineTradeSettlement")
- tax = sub(ls, RAM, "ApplicableTradeTax")
- sub(tax, RAM, "TypeCode", "VAT")
- sub(tax, RAM, "CategoryCode", ln.vat_category)
- sub(tax, RAM, "RateApplicablePercent", amount_str(ln.vat_rate))
-
- smls = sub(ls, RAM, "SpecifiedTradeSettlementLineMonetarySummation")
- sub(smls, RAM, "LineTotalAmount", amount_str(net), currencyID=inv.currency)
-
- # ── 3b. ApplicableHeaderTradeAgreement ──────
- agr = sub(tx, RAM, "ApplicableHeaderTradeAgreement")
-
- def _party(parent_el, tag, party: Party):
- p = sub(parent_el, RAM, tag)
- sub(p, RAM, "Name", party.name)
- if party.siret:
- sr = sub(p, RAM, "SpecifiedLegalOrganization")
- sub(sr, RAM, "ID", party.siret, schemeID="0002")
- if party.vat_id:
- tr = sub(p, RAM, "SpecifiedTaxRegistration")
- sub(tr, RAM, "ID", party.vat_id, schemeID="VA")
- if party.address:
- addr = sub(p, RAM, "PostalTradeAddress")
- sub(addr, RAM, "PostcodeCode", party.address.postcode)
- sub(addr, RAM, "LineOne", party.address.line1)
- if party.address.line2:
- sub(addr, RAM, "LineTwo", party.address.line2)
- sub(addr, RAM, "CityName", party.address.city)
- sub(addr, RAM, "CountryID", party.address.country_code)
- return p
-
- _party(agr, "SellerTradeParty", inv.seller)
- _party(agr, "BuyerTradeParty", inv.buyer)
-
- if inv.payment_reference:
- bor = sub(agr, RAM, "BuyerOrderReferencedDocument")
- sub(bor, RAM, "IssuerAssignedID", inv.payment_reference)
-
- # ── 3c. ApplicableHeaderTradeDelivery ───────
- sub(tx, RAM, "ApplicableHeaderTradeDelivery")
-
- # ── 3d. ApplicableHeaderTradeSettlement ─────
- stl = sub(tx, RAM, "ApplicableHeaderTradeSettlement")
- sub(stl, RAM, "InvoiceCurrencyCode", inv.currency)
-
- # Moyens de paiement
- pm = sub(stl, RAM, "SpecifiedTradeSettlementPaymentMeans")
- sub(pm, RAM, "TypeCode", inv.payment_means_code)
- if inv.iban:
- acc = sub(pm, RAM, "PayeePartyCreditorFinancialAccount")
- sub(acc, RAM, "IBANID", inv.iban)
- if inv.bic:
- fi = sub(pm, RAM, "PayeeSpecifiedCreditorFinancialInstitution")
- sub(fi, RAM, "BICID", inv.bic)
-
- # TVA par taux (calcul à partir des lignes)
- tax_totals: dict[tuple, list] = {}
- for ln in inv.lines:
- key = (ln.vat_rate, ln.vat_category)
- tax_totals.setdefault(key, []).append(round(ln.quantity * ln.unit_price, 2))
-
- total_ht = 0.0
- total_tva = 0.0
- for (rate, cat), bases in tax_totals.items():
- base = round(sum(bases), 2)
- tva = round(base * rate / 100, 2)
- total_ht += base
- total_tva += tva
- tax = sub(stl, RAM, "ApplicableTradeTax")
- sub(tax, RAM, "CalculatedAmount", amount_str(tva), currencyID=inv.currency)
- sub(tax, RAM, "TypeCode", "VAT")
- sub(tax, RAM, "BasisAmount", amount_str(base), currencyID=inv.currency)
- sub(tax, RAM, "CategoryCode", cat)
- sub(tax, RAM, "RateApplicablePercent", amount_str(rate))
-
- total_ht = round(total_ht, 2)
- total_tva = round(total_tva, 2)
- total_ttc = round(total_ht + total_tva, 2)
-
- # Échéance
- if inv.due_date:
- stp = sub(stl, RAM, "SpecifiedTradePaymentTerms")
- ddt = sub(stp, RAM, "DueDateDateTime")
- sub(ddt, UDT, "DateTimeString", date_str(inv.due_date), format="102")
-
- # Totaux
- sums = sub(stl, RAM, "SpecifiedTradeSettlementHeaderMonetarySummation")
- sub(sums, RAM, "LineTotalAmount", amount_str(total_ht), currencyID=inv.currency)
- sub(sums, RAM, "TaxBasisTotalAmount",amount_str(total_ht), currencyID=inv.currency)
- sub(sums, RAM, "TaxTotalAmount", amount_str(total_tva), currencyID=inv.currency)
- sub(sums, RAM, "GrandTotalAmount", amount_str(total_ttc), currencyID=inv.currency)
- sub(sums, RAM, "DuePayableAmount", amount_str(total_ttc), currencyID=inv.currency)
-
- return etree.ElementTree(root)
-
-
-# ─────────────────────────────────────────────
-# 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="FR12345678901",
- 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",
- ),
- ),
-
- lines=[
- InvoiceLine(
- line_id="1",
- description="Développement logiciel - Sprint 1",
- quantity=10,
- unit_code="HUR", # heures
- unit_price=150.00,
- vat_rate=0.0,
- ),
- InvoiceLine(
- line_id="2",
- description="Licence logicielle annuelle",
- quantity=1,
- unit_code="C62", # unité
- unit_price=500.00,
- vat_rate=0.0,
- ),
- InvoiceLine(
- line_id="3",
- description="Formation utilisateurs",
- quantity=2,
- unit_code="HUR",
- unit_price=200.00,
- vat_rate=0.0,
- ),
- ],
-
- payment_means_code="30", # 30 = virement bancaire
- iban="FR7612345678901234567890189",
- bic="BNPAFRPPXXX",
- payment_reference="CTR-2024-01",
- )
-
- tree = build_facturx(invoice)
- output_file = "factur-x.xml"
- tree.write(output_file, xml_declaration=True, encoding="UTF-8", pretty_print=True)
-
- print(f"✅ Fichier généré : {output_file}")
- print(f" Facture : {invoice.number}")
- print(f" Émetteur : {invoice.seller.name}")
- print(f" Destinataire : {invoice.buyer.name}")
-
- # Calcul récapitulatif
- total_ht = sum(round(l.quantity * l.unit_price, 2) for l in invoice.lines)
- total_tva = round(total_ht * 0.20, 2)
- print(f" Total HT : {total_ht:.2f} €")
- print(f" TVA 20% : {total_tva:.2f} €")
- print(f" Total TTC : {total_ht + total_tva:.2f} €")
\ No newline at end of file
diff --git a/routers/factures.py b/routers/factures.py
index 0a5e89c..94099e4 100644
--- a/routers/factures.py
+++ b/routers/factures.py
@@ -10,8 +10,8 @@ from models import Facture, LigneFacture, Client, StatutFacture
from numerotation import generer_numero_facture
from config import settings
from auth import get_current_user
-from template_helper import render
-from generate_facturx_basic import Address, Party, Invoice, InvoiceLine, build_facturx
+from template_helper import render, render_xml
+from generate_facturx_jinja2 import Address, Party, Invoice, InvoiceLine, generate_facturx_xml
router = APIRouter(prefix="/factures", tags=["factures"], dependencies=[Depends(get_current_user)])
templates = Jinja2Templates(directory="templates")
@@ -156,9 +156,29 @@ def changer_statut_facture(
db.commit()
return RedirectResponse(f"/factures/{facture_id}", status_code=303)
+from generate_facturx_jinja2 import Invoice, Party, Address, InvoiceLine, filter_amount, filter_datefmt
-@router.get("/{facture_id}/pdf")
-def telecharger_pdf(facture_id: int, db: Session = Depends(get_db)):
+@router.get("/{facture_id}/facturx")
+def telecharger_facturx(request: Request, facture_id: int, db: Session = Depends(get_db)):
+ facture = get_invoice_data(facture_id, db) # votre appel BDD
+
+ # facture = db.query(Facture).get(facture_id)
+ if not facture:
+ raise HTTPException(status_code=404)
+
+ xml_bytes = generate_facturx_xml(facture)
+
+ filename=f"factur-x-{facture_id}.xml"
+
+ return Response(
+ content= xml_bytes,
+ media_type="application/xml",
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'}
+ )
+
+
+@router.get("/{facture_id}/pdf", response_class=HTMLResponse)
+def telecharger_pdf(request: Request, facture_id: int, db: Session = Depends(get_db)):
from weasyprint import HTML
facture = db.query(Facture).get(facture_id)
if not facture:
@@ -173,10 +193,6 @@ def telecharger_pdf(facture_id: int, db: Session = Depends(get_db)):
filename = f"facture-{facture.numero}.pdf"
- tree = build_facturx(generate_xml (facture_id, db))
- output_file = "factur-x.xml"
- tree.write(output_file, xml_declaration=True, encoding="UTF-8", pretty_print=True)
-
return Response(
content=pdf_bytes,
media_type="application/pdf",
@@ -194,7 +210,7 @@ def apercu_pdf(request: Request, facture_id: int, db: Session = Depends(get_db))
"settings": settings,
})
-def generate_xml(facture_id: int, db: Session):
+def get_invoice_data(facture_id: int, db: Session):
facture = db.query(Facture).get(facture_id)
invoice = Invoice(
number=facture.numero,
@@ -228,11 +244,8 @@ def generate_xml(facture_id: int, db: Session):
),
),
-
-
payment_means_code="30", # 30 = virement bancaire
iban=settings.asso_iban,
- bic=settings.asso_bic,
payment_reference=facture.devis_origine.numero,
)
@@ -243,6 +256,7 @@ def generate_xml(facture_id: int, db: Session):
quantity=l.quantite,
unit_code="C62",
unit_price=l.prix_unitaire_ht,
+ vat_category="E",
vat_rate=0.0,
))
diff --git a/template_helper.py b/template_helper.py
index 3dd3c16..7330321 100644
--- a/template_helper.py
+++ b/template_helper.py
@@ -3,7 +3,7 @@ Helper pour injecter automatiquement current_user dans le contexte Jinja2.
"""
from fastapi import Request
from fastapi.templating import Jinja2Templates
-from starlette.responses import HTMLResponse
+from starlette.responses import HTMLResponse, Response
def render(templates: Jinja2Templates, template_name: str,
@@ -17,3 +17,20 @@ def render(templates: Jinja2Templates, template_name: str,
response = templates.TemplateResponse(template_name, ctx)
response.status_code = status_code
return response
+
+def render_xml(templates: Jinja2Templates, template_name: str,
+ request: Request, invoice: "Invoice",
+ filename: str = "factur-x.xml") -> Response:
+
+ xml_content = templates.env.get_template(template_name).render(
+ request=request,
+ invoice=invoice,
+ seller=invoice.seller,
+ buyer=invoice.buyer,
+ )
+
+ return Response(
+ content=xml_content,
+ media_type="application/xml",
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'}
+ )
diff --git a/templates/factures/liste.html b/templates/factures/liste.html
index 99ed8bf..355891b 100644
--- a/templates/factures/liste.html
+++ b/templates/factures/liste.html
@@ -29,6 +29,7 @@
Voir
PDF
+ facturx
|
{% endfor %}