Skill e-facture-rapprochement
Pipeline complet factures + banque → rapprochement → rapprochement.json, pour un ou
plusieurs clients. Cas d'usage dominant : photos de tickets de caisse et PDF côté
factures, exports bancaires (CSV/CAMT/OFX) ou PDF côté banque.
> Le moteur est dans scripts/main.py : ingestion (invoice_parsers.py,
> bank_parsers.py) + extraction LLM des pièces non structurées. Le RAPPROCHEMENT
> lui-même est délégué au moteur unique et audité du skill rapprochement-paiements (⑤)
> — un seul moteur pour les deux skills, donc des sorties identiques à entrées
> identiques (politique conservatrice unique : « payé » seulement sur montant serré,
> exclusions prudentes). Ce skill = comment le lancer + faire l'extraction LLM + rendre
> compte. Ne réimplémente pas le rapprochement et n'écris jamais rapprochement.json
> à la main : le format est contractuel (le backend l'ignore en silence s'il est malformé
> → le client perd ses données). main.py garantit le format et l'invariant.
> (reconcile.py reste présent pour ses tests unitaires mais n'écrit plus la sortie.)
Les 4 étapes (A → B → C → D)
- A. Ingestion factures — cascade : Factur-X (XML CII embarqué) → UBL/CII pur → LLM
(texte/vision) pour les tickets/PDF, avec score de confiance + validation humaine au-delà
d'un seuil. Voirreferences/invoice-extraction.md. - B. Ingestion banque — CAMT.053 / OFX / CSV → transactions normalisées (montant signé).
PDF de relevé → extraction LLM. Voirreferences/bank-formats.md. - C. Rapprochement — délégué au moteur partagé ⑤ (
rapprochement-paiements) :
match montant + (référence/contrepartie/date), jamais le montant seul ; non matché →
unmatched_bank_lines; non facturable (salaires, impôts, frais bancaires, retraits…) →
excluded_bank_lines. Politique conservatrice. Voirreferences/matching-rules.md. - D. Sortie —
company.json+rapprochement.json, invariant vérifié. Voir
references/output-contract.md.
Disposition d'entrée (par client)
<root>/<slug>/invoices|factures/ → factures (pdf, jpg, png, xml…)
<root>/<slug>/bank|releves|banque/ → relevés (xml CAMT, ofx, csv, pdf…)
<root>/<slug>/company.json → identité (optionnel ; sert à déduire achat/vente)
<root> par défaut = sandbox ~/.openclaw/workspace/clients-test/ (pour ne pas écraser
d'autres sorties pendant la mise au point). Ajoute --real pour viser le vrai
~/.openclaw/workspace/clients/. Slug = minuscules-à-tirets ; dossiers _* ignorés.
Exécution — boucle en DEUX temps
Les tickets/PDF passent par TON extraction LLM, donc le run se fait en deux passes.
Passe 1 — lancer le moteur :
python3 scripts/main.py [<root>] [--client <slug>] [--today AAAA-MM-JJ]
Le script ingère tout ce qui est structuré. S'il reste des documents non structurés (photos,
PDF sans XML), il s'arrête et imprime une worklist : pour chacun, un chemin sidecar
où écrire le JSON extrait.
Passe 2 — extraire puis relancer :
Pour CHAQUE entrée de la worklist :
1. Lis le document (mode=text → couche texte via pdftotext -layout ; mode=vision →
lis l'image).
2. Extrais au schéma normalisé (references/invoice-extraction.md pour une facture,
references/bank-formats.md pour un relevé), avec un confidence honnête.
3. Écris le JSON dans le chemin sidecar indiqué (<doc>.<ext>.invoice.json ou .bank.json).
Puis relance la même commande. Quand la worklist est vide, main.py rapproche, écrit les
2 fichiers, et imprime l'auto-contrôle. Les sidecars sont un cache : un doc déjà extrait
n'est pas redemandé.
Après l'exécution — rendre compte
Lis l'auto-contrôle et relaie au comptable un tableau lisible par client : factures
payées / en attente / partielles / en retard, paiements orphelins et factures manquantes,
relances. Mets en avant les factures listées pour validation humaine (_review.json :
montant élevé ou extraction peu sûre) — un humain doit les confirmer. Jamais de chemins
techniques bruts.
Règles critiques
- Format de sortie = contrat. N'écris pas
rapprochement.json/company.jsonà la main ;
laissemain.pyle faire (écriture atomique, snake_case exact). Pas de champblocking
(supprimé du produit). - Invariant non négociable. Pour chaque période,
rappr + unmatched + excluded == bank_transactions_count(aucune transaction oubliée).
main.pyl'affiche (OK/KO) et sort en erreur si une période est KO — ne termine pas
sur un KO ni sur un JSON invalide. - N'invente jamais un montant. Ticket illisible → sidecar
confidence:0.0sans montant →
anomaliefacture_illisible(pièce à re-fournir). Mieux vaut une anomalie qu'un faux chiffre. - Confiance honnête. Elle pilote la validation humaine (seuils dans
normalize.py:
HUMAN_REVIEW_AMOUNT,HUMAN_REVIEW_CONFIDENCE). - Sandbox d'abord. Sans
--real, tout va dansclients-test/; on bascule sur le vrai
clients/une fois validé. - Si le script échoue, signale l'erreur exacte (stderr) ; ne rejoue pas le rapprochement à la main.
Tests
python3 scripts/test_reconcile.py # moteur (montants, dates, matching, invariant, TVA)
python3 scripts/test_e2e.py # bout-en-bout sur fixtures/ (UBL+CII+CAMT+CSV+ticket)
Périmètre
Ce skill produit seulement company.json + rapprochement.json ; il ne touche ni au
backend, ni au front, ni aux e-mails. Il complète l'écosystème compta existant (voir README)
en se concentrant sur l'ingestion e-facture/ticket structurée + LLM.