Skill: trendex/organisation-documents (version 0.2.0)

--- name: organisation-documents description: Skill central de l'assistant comptable. Réceptionne, classe et nomme automatiquement les documents comptables (factures, relevés bancaires) par client / année / mois / nature, et déduit clients.json depuis les relevés bancaires. À INVOQUER SYSTÉMATIQUEMENT et SANS DEMANDER L'AUTORISATION dès que (a) un e-mail arrive avec une PJ PDF/image, (b) un e-mail contient un mot-clé comptable (facture, invoice, reçu, paiement, TVA, total, acompte, avoir, relevé), (c) le comptable demande de traiter / classer / organiser un document, (d) un fichier est déposé dans l'inbox, (e) le comptable répond à une question d'identification de client. L'extraction et le classement sont faits par scripts/main.py + scripts/extract.py — aucun champ n'est jamais deviné à l'œil. license: Interne — usage privé OpenClaw --- # 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 appelle `scripts/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 :** ```bash 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 de `F1-2026-0001` ; `total_ttc` = `0.00` au lieu du vrai montant). Ces erreurs cassent ensuite tout le rapprochement de `rapprochement-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_id`** quand le PDF n'en contient pas de lisible. Si `extract.py` ne le trouve pas, le script écrit `SANS-NUM` ; n'essaie PAS de fabriquer mieux à partir du nom de l'émetteur ou de la description. ### ✅ OBLIGATOIRE 1. Exécuter la commande shell ci-dessus (et UNIQUEMENT ça pour la partie classement). 2. Lire le `_report.json` produit. 3. Relayer au comptable un résumé court + les questions d'onboarding si la section `questions` n'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_id` bidons (`N`, `des`, `um-rix`). À chaque fois, `rapprochement-bancaire` flagge ensuite des `facture_manquante` qui 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 : 1. E-mail avec PJ → traitement immédiat. 2. Dépôt manuel dans l'inbox → idem. 3. Import en masse (ZIP, dossier) → idem. 4. Réponse du comptable à une question d'identification → reprise (étape 2 de l'onboarding). 5. Reclassement / correction → relancer le script. Ne **pas** utiliser pour : FEC (→ `fec-parser`), relances (→ `relances`), rapprochement bancaire (→ `rapprochement-bancaire`). --- ## Exécution ```bash python3 scripts/main.py <dossier_inbox> [<racine_clients>] # racine_clients par défaut : ~/.openclaw/workspace/clients ``` Le script : 1. **Dédoublonne** par SHA-256 (ignore les fichiers déjà classés via `_index.json`). 2. **Extrait** chaque PDF avec `scripts/extract.py` (`pdftotext -layout` + règles déterministes). Aucun montant, numéro ou nom n'est inventé. 3. **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/`. 4. **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. 5. **Phase 3 — autres** : ni facture ni relevé reconnaissable → `_non-attribue/`. 6. É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 → questions` n'est pas vide → poser ces questions au comptable (cf. format) ; - si `_report.json → incomplete` n'est pas vide → signaler ces pièces ; - pour chaque facture classée (hors `_a-identifier`/`_incomplet`), considérer le post-traitement `rapprochement-bancaire` dé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) : 1. **Relevé bancaire** : titulaire du compte → certitude. 2. **Mapping confirmé** : e-mail expéditeur ∈ `contacts[].email`, domaine ∈ `domains`, ou SIREN du document ∈ `siren` d'un client. 3. **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** » : 1. Ajouter/compléter l'entrée `clients.json` de X : ```json { "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"] } ``` 2. Relancer `python3 scripts/main.py clients/_a-identifier <racine_clients>` : avec X désormais dans `clients.json`, les pièces de `_a-identifier/` liées sont classées (sens in/out déterminé) et sortent du dossier. 3. 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 ```json [ { "slug": "corse-plomberie", "raisonSociale": "Corse Plomberie", "statut": "auto-from-bank-statement | confirmed", "confiance": 0.9, "aValider": false, "siren": ["812345678"], "contacts": [{ "email": "[REDACTED_SECRET]" }], "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 (ex `F1-2026-0003`), alphanumérique+tirets ; `SANS-NUM` si 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 (ex `3336.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É`, ou `Solde 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` · bloc `SIRET`/`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 : ```json { "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 : ```json { "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.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)