Aller au contenu

Guide

Modèle de facture HTML avec un exemple PDF fonctionnel

Un modèle de facture complet en HTML et CSS à télécharger et à adapter, le PDF qu'il produit, la requête qui le génère et le CSS d'impression qui garde les longues factures soignées d'une page à l'autre.

Si une facture dispose déjà d'une vue web en HTML, le même balisage et les mêmes styles peuvent servir pour le PDF. Les versions navigateur et PDF restent ainsi alignées, sans mise en page séparée à maintenir. Ironfang Render l'imprime dans Chromium en appliquant vos styles d'impression, puis renvoie le PDF terminé.

Télécharger l'exemple fonctionnel

  • invoice.html : une facture A4 d'une page : HTML et CSS autonomes, sans polices ni images externes
  • invoice-multipage.html : le même design avec 48 lignes, qui occupe quatre pages
  • request.json : le corps exact de la requête pour invoice.html, construit avec la commande ci-dessous
  • invoice.pdf : le PDF que produit cette requête, une page
  • invoice-multipage.pdf : le modèle multipage généré avec les mêmes options, quatre pages
Page 1 du PDF d'exemple : facture INV-1042 de Northwind Studio Ltd à Harbour Lane Retail Ltd, six lignes, 11 175,00 livres hors taxes, TVA à 20 % : 2 235,00, total dû 13 410,00, et "Page 1 of 1" dans le pied de page.
La première page du fichier invoice.pdf.

Adapter le modèle

  • Vendeur et acheteur : les trois blocs de section.parties contiennent les noms, les adresses, le numéro de TVA et les informations de la facture. Remplacez le texte ; la mise en page n'en dépend pas.
  • Lignes : chaque tr de tbody est une ligne de facture, avec la description et une courte ligne de détail dans td.desc. Ajoutez ou supprimez des lignes librement ; le modèle multipage en montre 48.
  • Totaux : le modèle ne calcule rien. Calculez les montants des lignes, la TVA et le total dans votre propre code avec une arithmétique décimale, puis inscrivez les résultats.
  • Identité visuelle : la couleur du bandeau d'en-tête est #1e3a5f et le logo est un SVG en ligne. Remplacez les deux, en gardant le logo en ligne ou sous forme de data URI pour qu'il ne nécessite aucun téléchargement.

Un PDF comme celui-ci est l'image d'une facture, destinée à être lue par des personnes. Lorsqu'un acheteur ou un pays exige une facture électronique structurée, il s'agit d'un document XML distinct, comme une facture Peppol BIS Billing 3 : l'exemple de facture XML Peppol en présente une, expliquée champ par champ.

Générer le PDF

jq construit le corps de la requête à partir du modèle et échappe le HTML dans une chaîne JSON : le balisage n'a jamais besoin d'être échappé à la main. Envoyez-le ensuite avec votre clé, lue dans la variable d'environnement IRONFANG_API_KEY.

export IRONFANG_API_KEY="if_live_..."

jq -n --rawfile html invoice.html '{
  html: $html,
  paper_format: "a4",
  print_background: true,
  margin: { top: 0.6, right: 0.5, bottom: 0.8, left: 0.5 },
  header_html: "<span></span>",
  footer_html: "<div style=\"font-size:9px;width:100%;text-align:center;color:#666;font-family:sans-serif\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
}' > request.json

curl -sS https://api.ironfang.uk/render/v1/pdf \
  -H "Authorization: Bearer $IRONFANG_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @request.json \
  -o invoice.pdf \
  -w '%{http_code} %{content_type}\n'

Un code 200 avec application/pdf signifie que invoice.pdf est le document. Tout autre statut est une erreur JSON, {"error": {"code", "message"}}, écrite dans le même fichier : lisez-la au lieu de l'ouvrir comme un PDF. Chaque PDF consomme deux crédits, et un rendu échoué est remboursé.

Taille de page, marges et numéros de page

paper_format accepte a3, a4, a5, letter, legal ou tabloid ; sans ce champ, la page est au format Letter. Les marges sont en pouces, pour chaque côté. Le pied de page est dessiné à l'intérieur de la marge inférieure, prévoyez donc de la place : ici, 0,8 pouce tient le numéro de page à l'écart de la dernière ligne.

Chromium remplit les classes pageNumber, totalPages, date, title et url dans les modèles d'en-tête et de pied de page. Envoyer l'un des deux modèles active les deux : une requête avec seulement un pied de page a donc aussi besoin d'un header_html vide, comme ci-dessus. Sinon, Chromium imprime sa date et le titre de la page par défaut en haut de chaque page.

Les longues factures sur plusieurs pages

Trois règles du modèle font tenir une longue facture sur plusieurs pages. Les en-têtes de colonnes se répètent en haut de chaque page, une ligne de facture n'est jamais coupée entre deux pages, et les totaux restent groupés avec les informations de paiement. Dans invoice-multipage.pdf, les en-têtes ouvrent les quatre pages, les longues descriptions de cinq lignes restent entières et les totaux arrivent ensemble en page quatre.

Extrait de invoice-multipage.html.

table.items thead { display: table-header-group; } /* repeat the headings */
table.items tr { break-inside: avoid; }            /* keep each line whole */
.end, .totals { break-inside: avoid; }             /* totals with payment details */

Une ligne plus haute qu'une page doit tout de même être coupée. Limitez les descriptions à quelques lignes et placez les conditions longues dans une section distincte, après les totaux.

Arrière-plans, polices et images

  • Arrière-plans : par défaut, les navigateurs omettent à l'impression les couleurs et images d'arrière-plan. Envoyez print_background: true et ajoutez print-color-adjust: exact aux éléments qui en portent, comme un bandeau d'en-tête aux couleurs de la marque ou un encadré de totaux ombré.
  • Polices : le moteur de rendu a ses propres polices installées, pas les vôtres. Pour un rendu exact, chargez votre police avec @font-face depuis une URL publique ou une data URI, et définissez wait_until sur networkidle pour qu'elle soit arrivée avant l'impression de la page. L'exemple utilise une pile de polices système, c'est pourquoi il ne nécessite aucun téléchargement.
  • Images : un SVG en ligne ou une data URI s'imprime sans requête. Une image référencée par URL doit être accessible depuis l'internet public.

Remplir le modèle en toute sécurité

Les noms des clients, les adresses et les descriptions des lignes proviennent de vos données, et un seul chevron égaré peut casser la mise en page ou injecter du balisage. Échappez chaque valeur au moment de l'insérer dans le HTML, exactement comme pour une page web.

Python, avec la bibliothèque standard.

from html import escape

row = (
    f"<tr><td>{escape(item.description)}</td>"
    f"<td>{item.quantity}</td><td>{item.unit_price:,.2f}</td></tr>"
)

Dépannage

  • Une date et le titre de la page apparaissent en haut de chaque page : la requête a un footer_html mais pas de header_html. Ajoutez "header_html": "<span></span>".
  • Le numéro de page chevauche la dernière ligne : la marge inférieure est trop petite pour le pied de page. Augmentez margin.bottom.
  • Les couleurs ou le bandeau d'en-tête manquent : définissez print_background à true.
  • Le bord droit est coupé : un élément a une largeur fixe plus grande que le papier. Utilisez des pourcentages ou max-width: 100% pour le conteneur de la page et les tableaux.
  • Une ligne de facture est coupée sur deux pages : ajoutez break-inside: avoid à tr, et vérifiez qu'aucune ligne n'est plus haute qu'une page.
  • Le PDF s'ouvre comme une erreur : la réponse était du JSON, pas un PDF. Vérifiez le code de statut qu'affiche la commande curl.

Consommation de crédits

Chaque PDF consomme deux crédits, et les rendus échoués sont remboursés. Les PDF ne sont pas mis en cache : chaque requête génère à nouveau le document. Conservez donc le PDF que vous émettez et servez les téléchargements suivants depuis votre copie plutôt que de le générer de nouveau.

Pour aller plus loin