Leçon 3 — Construire pas à pas

Les contrôles de cohérence et les écritures CSV

5 min de lecture

Les contrôles de cohérence et les écritures CSV

Les cinq contrôles

files/factures.py
TAUX_TVA_FR = {0.0, 2.1, 5.5, 10.0, 20.0}
TOLERANCE = 0.01  # euro : écarts d'arrondi acceptés

def controler(f: Facture, aujourdhui: date) -> tuple[list[str], list[str]]:
    """Renvoie (anomalies bloquantes, alertes)."""
    bloquantes, alertes = [], []
    for l in f.lignes:
        if abs(l.quantite * l.prix_unitaire_ht - l.montant_ht) > TOLERANCE:
            bloquantes.append(f"ligne « {l.designation} » : {l.quantite} x {l.prix_unitaire_ht} != {l.montant_ht}")
    somme = round(sum(l.montant_ht for l in f.lignes), 2)
    if abs(somme - f.total_ht) > TOLERANCE:
        bloquantes.append(f"total HT imprimé {f.total_ht:.2f} != somme des lignes {somme:.2f}")
    if f.taux_tva not in TAUX_TVA_FR:
        bloquantes.append(f"taux de TVA inattendu : {f.taux_tva} %")
    tva_attendue = round(f.total_ht * f.taux_tva / 100, 2)
    if abs(tva_attendue - f.montant_tva) > TOLERANCE:
        bloquantes.append(f"TVA imprimée {f.montant_tva:.2f} != {f.taux_tva:g} % du HT ({tva_attendue:.2f})")
    if abs(f.total_ht + f.montant_tva - f.total_ttc) > TOLERANCE:
        bloquantes.append(f"TTC {f.total_ttc:.2f} != HT + TVA ({f.total_ht + f.montant_tva:.2f})")
    if f.date_echeance is None:
        alertes.append("date d'échéance absente")
    elif f.date_echeance < f.date_facture:
        bloquantes.append("échéance antérieure à la date de facture")
    elif f.date_echeance < aujourdhui:
        alertes.append(f"échéance dépassée depuis le {f.date_echeance.isoformat()}")
    return bloquantes, alertes
ContrôleTypeExemple dans le lab
quantité × prix unitaire = montant de la lignebloquantaucune facture fautive
somme des lignes = total HTbloquantF-2026-0413
taux de TVA dans la liste des taux françaisbloquantaucune
HT × taux = TVAbloquantF-2026-0413, F-2026-0414
HT + TVA = TTCbloquantaucune (les TTC imprimés sont cohérents avec leurs HT et TVA)
échéance absente ou dépasséealerteF-2026-0415

aujourdhui est un paramètre plutôt qu'un appel à date.today() dans la fonction : le test peut fixer la date (date(2026, 10, 15)) et rester reproductible le mois prochain.

Une facture a un taux de TVA de 19,6 % (ancien taux français) avec des montants cohérents entre eux. Que renvoie controler ?

Les écritures comptables

Une facture d'achat se traduit en comptabilité française par trois écritures (lignes du journal des achats, code HA), selon le plan comptable général :

CompteIntituléDébitCrédit
607000Achats de marchandisesHT
445660TVA déductible sur autres biens et servicesTVA
401000FournisseursTTC

Le total des débits égale le total des crédits : l'écriture est équilibrée. Le choix du compte de charge (607 ici) dépend de la nature de l'achat ; un vrai service comptable affinerait par fournisseur ou par catégorie.

files/factures.py
def ecritures(f: Facture) -> list[dict]:
    base = {"journal": "HA", "date": f.date_facture.isoformat(), "piece": f.numero,
            "libelle": f"{f.fournisseur} {f.numero}"}
    return [{**base, "compte": "607000", "debit": f"{f.total_ht:.2f}", "credit": ""},      # achats HT
            {**base, "compte": "445660", "debit": f"{f.montant_tva:.2f}", "credit": ""},   # TVA déductible
            {**base, "compte": "401000", "debit": "", "credit": f"{f.total_ttc:.2f}"}]     # fournisseur TTC
flowchart LR
  F["Facture F-2026-0412 : HT 138,00 / TVA 27,60 / TTC 165,60"] --> E1["607000 débit 138,00"]
  F --> E2["445660 débit 27,60"]
  F --> E3["401000 crédit 165,60"]
  E1 --> B{"138,00 + 27,60 = 165,60 ?"}
  E2 --> B
  E3 --> B
  B -->|"oui"| OK["écriture équilibrée"]

Écrire les fichiers

files/factures.py
if not bloquantes:
    journal += ecritures(f)  # seules les factures cohérentes partent en comptabilité
...
with open(sortie / "ecritures.csv", "w", newline="", encoding="utf-8") as fh:
    w = csv.DictWriter(fh, fieldnames=["journal", "date", "piece", "compte", "libelle", "debit", "credit"],
                       delimiter=";")
    w.writeheader()
    w.writerows(journal)

Le séparateur ; est l'usage le plus courant des imports comptables en France, car la virgule y sert de séparateur décimal. Vérifiez le format attendu par votre logiciel (ordre des colonnes, décimales, encodage) : il varie d'un éditeur à l'autre.

Pourquoi la facture F-2026-0415 (échéance dépassée) figure-t-elle dans ecritures.csv ?

Exercice : un contrôle de doublon

Ajoutez à traiter un contrôle qui rend bloquante une facture dont le couple (fournisseur, numéro) a déjà été vu dans le même lot.

Voir la solution
vus = set()
for pdf in sorted(dossier.glob("*.pdf")):
    f = await extraire(runner, texte_pdf(pdf))
    bloquantes, alertes = controler(f, aujourdhui)
    cle = (f.fournisseur.strip().lower(), f.numero.strip())
    if cle in vus:
        bloquantes.append("facture en double dans le lot")
    vus.add(cle)
    ...

En production, vus serait chargé depuis l'historique (les pièces déjà présentes dans les écritures des mois précédents), pas seulement le lot courant.

Résultat obtenu

sortie/ecritures.csv réellement produit le 2026-10-15 :

journal;date;piece;compte;libelle;debit;credit
HA;2026-09-30;F-2026-0412;607000;Papeterie des Canuts F-2026-0412;138.00;
HA;2026-09-30;F-2026-0412;445660;Papeterie des Canuts F-2026-0412;27.60;
HA;2026-09-30;F-2026-0412;401000;Papeterie des Canuts F-2026-0412;;165.60
HA;2026-08-20;F-2026-0415;607000;Nettoyage Confluence F-2026-0415;480.00;
HA;2026-08-20;F-2026-0415;445660;Nettoyage Confluence F-2026-0415;96.00;
HA;2026-08-20;F-2026-0415;401000;Nettoyage Confluence F-2026-0415;;576.00

Deux factures sur quatre partent en comptabilité ; les deux autres attendent un humain, avec la raison écrite dans anomalies.json.

Je sais faire

  • Écrire des contrôles de cohérence avec une tolérance d'arrondi
  • Séparer anomalies bloquantes et alertes
  • Traduire une facture d'achat en trois écritures équilibrées
  • Écrire un CSV au format attendu par la comptabilité
Suivant