Skill organisation-documents
Moteur d'entrée du domaine comptable. Réception → identification du client → classement → indexation → rapport. Le travail réel est fait par
scripts/main.py(qui appellescripts/extract.py). Ce skill = quand le lancer + comment dialoguer avec le comptable.
⚠️ Règle absolue — EXÉCUTION DU SCRIPT (non négociable)
Pour chaque invocation, la SEULE action de classement correcte est d'exécuter la commande :
python3 scripts/main.py <dossier_inbox> <racine_clients>
puis de lire <racine_clients>/_report.json et de le relayer au comptable.
❌ INTERDIT
- Lire les PDFs un par un et classer à la main. L'agent extrait les champs à l'œil de façon inconsistante (cas réels déjà observés :
invoice_id="N","des","um-rix"au lieu deF1-2026-0001;total_ttc=0.00au lieu du vrai montant). Ces erreurs cassent ensuite tout le rapprochement derapprochement-bancaire. - Dupliquer la logique du script en Python ad-hoc dans une cellule / un sous-process inline. Le script EST la logique, il est déterministe, déjà testé. Le réimplémenter à chaque invocation est garanti de diverger.
- Créer ou déplacer des fichiers dans
<racine_clients>/à la main (move, copy, write). C'est le script qui le fait. - Inventer un
invoice_idquand le PDF n'en contient pas de lisible. Siextract.pyne le trouve pas, le script écritSANS-NUM; n'essaie PAS de fabriquer mieux à partir du nom de l'émetteur ou de la description.
✅ OBLIGATOIRE
- Exécuter la commande shell ci-dessus (et UNIQUEMENT ça pour la partie classement).
- Lire le
_report.jsonproduit. - Relayer au comptable un résumé court + les questions d'onboarding si la section
questionsn'est pas vide.
Si le script échoue
Signale l'erreur exacte (stderr) au comptable. NE PAS "rattraper" en classant manuellement — c'est précisément ce qui produit les filenames cassés. Si le binaire pdftotext manque (poppler-utils non installé), demande-le et stoppe.
Pourquoi cette règle est aussi stricte : on a déjà eu plusieurs runs où l'agent a improvisé le classement et produit des
invoice_idbidons (N,des,um-rix). À chaque fois,rapprochement-bancaireflagge ensuite desfacture_manquantequi n'en sont pas, le comptable perd du temps à investiguer. Le script ne fait pas ces erreurs.
Quand utiliser ce skill
Réflexe par défaut face à tout document entrant :
- E-mail avec PJ → traitement immédiat.
- Dépôt manuel dans l'inbox → idem.
- Import en masse (ZIP, dossier) → idem.
- Réponse du comptable à une question d'identification → reprise (étape 2 de l'onboarding).
- Reclassement / correction → relancer le script.
Ne pas utiliser pour : FEC (→ fec-parser), relances (→ relances), rapprochement bancaire (→ rapprochement-bancaire).
Exécution
python3 scripts/main.py <dossier_inbox> [<racine_clients>]
# racine_clients par défaut : ~/.openclaw/workspace/clients
Le script :
- Dédoublonne par SHA-256 (ignore les fichiers déjà classés via
_index.json). - Extrait chaque PDF avec
scripts/extract.py(pdftotext -layout+ règles déterministes). Aucun montant, numéro ou nom n'est inventé. - Phase 1 — relevés bancaires d'abord : le titulaire affiché en en-tête du relevé EST le client du cabinet (identité certaine) → crée/complète l'entrée
clients.json(statut: auto-from-bank-statement), classe le relevé dans<slug>/<AAAA>/<MM>/bank-statements/. - Phase 2 — factures : pour chaque facture, compare émetteur et destinataire à
clients.json(exact puis fuzzy ≥ 0.82).- un seul côté matche → c'est le client ; émetteur ≈ client →
invoices/out/, destinataire ≈ client →invoices/in/. - aucun côté ne matche →
_a-identifier/+ une question dans le rapport (cf. onboarding ci-dessous). - les deux matchent →
_a-identifier/+ question (cas rare). - extraction incomplète (date ou montant TTC introuvable) →
_incomplet/+ ligne dans le rapport.
- un seul côté matche → c'est le client ; émetteur ≈ client →
- Phase 3 — autres : ni facture ni relevé reconnaissable →
_non-attribue/. - Écrit
clients/clients.json,clients/_index.json,clients/_report.json, et imprime un résumé.
Après l'exécution, lire clients/_report.json et :
- relayer au comptable un résumé court (nb de factures classées, nb de relevés, clients créés) ;
- si
_report.json → questionsn'est pas vide → poser ces questions au comptable (cf. format) ; - si
_report.json → incompleten'est pas vide → signaler ces pièces ; - pour chaque facture classée (hors
_a-identifier/_incomplet), considérer le post-traitementrapprochement-bancairedéclenché (le batch repassera de toute façon).
Identification du client — règle absolue
Trois signaux fiables, dans l'ordre (gérés par le script) :
- Relevé bancaire : titulaire du compte → certitude.
- Mapping confirmé : e-mail expéditeur ∈
contacts[].email, domaine ∈domains, ou SIREN du document ∈sirend'un client. - Raison sociale fuzzy ≥ 0.82 contre
clients.json— seulement si un seul des deux côtés matche.
Si aucun signal fiable → on ne devine pas. Le document part en _a-identifier/ et le comptable tranche une fois.
Onboarding d'un expéditeur ambigu (le cas « Corse Plomberie »)
Une facture contient toujours deux entreprises (émetteur, destinataire). Quand aucune n'est connue — et qu'aucun relevé ne couvre l'une d'elles — le script ne choisit pas : il met la pièce dans _a-identifier/ et ajoute une question.
Étape 1 (automatique) — la question apparaît dans _report.json → questions :
« Document : facture
TUYO-2024-087(348,50 € TTC). Émetteur « TUYO SARL », destinataire « Corse Plomberie ». Lequel est votre client ? »
L'agent la relaie au comptable. Aucune écriture dans clients.json à ce stade.
Étape 2 (à la réponse du comptable) — quand le comptable répond « c'est X » :
- Ajouter/compléter l'entrée
clients.jsonde X :{ "slug": "<slug X>", "raisonSociale": "X", "statut": "confirmed", "confiance": 1.0, "aValider": false, "siren": ["<si lisible sur la pièce>"], "contacts": [{"email": "<expéditeur>"}], "domains": ["<si e-mail pro>"], "sources": ["accountant-confirmation"] } - Relancer
python3 scripts/main.py clients/_a-identifier <racine_clients>: avec X désormais dansclients.json, les pièces de_a-identifier/liées sont classées (sens in/out déterminé) et sortent du dossier. - Relayer le résultat.
À partir de là, tout document futur du même expéditeur (même e-mail) est attribué automatiquement.
Si le comptable répond « aucune des deux »
Déplacer la pièce de _a-identifier/ vers _non-attribue/. Pas de suivi comptable.
clients.json — structure
[
{
"slug": "corse-plomberie",
"raisonSociale": "Corse Plomberie",
"statut": "auto-from-bank-statement | confirmed",
"confiance": 0.9,
"aValider": false,
"siren": ["812345678"],
"contacts": [{ "email": "jeanmichel@gmail.com" }],
"domains": ["corseplomberie.fr"],
"sources": ["bank-statement"]
}
]
Fichier construit et maintenu par ce skill seul (via scripts/main.py + ajouts manuels lors de l'onboarding). Aucun autre skill ne l'écrit. Le comptable ne l'édite pas à la main : il répond aux questions.
Arborescence cible
clients/
├── clients.json ← liste des clients (déduite des relevés + confirmations)
├── _index.json ← sha256 → chemin classé (dédup)
├── _report.json ← rapport de la dernière exécution
├── _a-identifier/ ← factures dont le client n'est pas confirmé (onboarding en cours)
├── _incomplet/ ← pièces dont l'extraction a échoué (date / montant manquant)
├── _non-attribue/ ← ni facture ni relevé exploitable
├── _cabinet/ ← documents internes du cabinet
└── <slug>/
└── <AAAA>/<MM>/
├── bank-statements/
│ └── <AAAA-MM>_<banque>.pdf
└── invoices/
├── in/
│ └── <AAAA-MM-JJ>_<N°Facture>_<Contrepartie>_<MontantTTC>.pdf
└── out/
└── <AAAA-MM-JJ>_<N°Facture>_<Contrepartie>_<MontantTTC>.pdf
Convention de nom (produite par le script, jamais à la main) :
N°Facture: numéro réel extrait du PDF (exF1-2026-0003), alphanumérique+tirets ;SANS-NUMsi absent.Contrepartie: 14 premiers caractères significatifs du nom de l'autre partie, sans accents/espaces.MontantTTC: point décimal, sans séparateur de milliers, sans symbole (ex3336.78).AAAA-MM-JJ: date d'émission du document.
Détection facture vs relevé (rappel — implémenté dans extract.py)
- Relevé bancaire si :
RELEVÉ DE COMPTE/EXTRAIT DE COMPTE/ACCOUNT STATEMENT, ou nom de banque +RELEVÉ, ouSolde d'ouverture/Solde de clôture. Exception : si le document contient aussi (FACTURE/INVOICE) ET (SIRET/TVA) → c'est une facture. - Facture si ≥ 2 signaux parmi :
FACTURE/INVOICE/BON DE FACTURATION· blocSIRET/TVA/SIREN· bloc destinataire (FACTURÉ À/DESTINATAIRE/BILL TO) ·TOTAL HT/TOTAL TTC/NET À PAYER. - Sinon →
_non-attribue/.
Communication avec le comptable
- Silence par défaut : une ligne au début, une ligne à la fin. Pas de narration par étape.
- Questions d'onboarding : regroupées en fin de message, une par expéditeur ambigu, format « Document … — Émetteur « … », destinataire « … » — lequel est votre client ? ».
- Vocabulaire interdit :
pdftotext,regex,SHA-256,pipeline,extract.py, chemins absolus système. - Vocabulaire métier : facture, pièce, dossier client, mois en cours, classement, doublon, mention obligatoire, contrepartie.
Garde-fous
- Aucune donnée ne quitte le container LXD.
- Conservation 10 ans minimum. Jamais de suppression —
_a-identifier/,_incomplet/,_non-attribue/conservent les pièces. - Sources externes en lecture seule.
- Le système ne devine jamais un montant, un numéro ou l'identité d'un client : signal fiable, sinon question.
_incomplet/plutôt qu'une valeur inventée.
Post-traitement
Pour chaque facture / relevé classé dans un vrai dossier client (pas _a-identifier/ ni _incomplet/), le moteur d'état rapprochement-bancaire reprendra. Émettre :
{ "trigger": "rapprochement-bancaire", "client": "<slug>" }
(Le batch repasse de toute façon sur tous les clients ; ce trigger ne fait qu'accélérer.)
Relances : ne jamais appeler directement. Produire au plus :
{ "trigger_suggestion": "relances", "reason": "invoice status may require update" }
Philosophie
organisation-documents = moteur d'entrée — classe les pièces, identifie les clients (relevé ou question), maintient clients.json
rapprochement-bancaire = moteur d'état — rapproche, reconstruit l'état comptable
relances = moteur de décision différée
Le travail mécanique est dans les scripts. Ce skill décide quand les lancer et comment en parler au comptable.
微信扫一扫