Moteur comptable quotidien du cabinet. Analyse les dossiers clients déjà classés par organisation-documents, rapproche les paiements avec les factures, valid...
---
name: rapprochement-bancaire
description: Moteur comptable quotidien du cabinet. Analyse les dossiers clients déjà classés par organisation-documents, rapproche les paiements avec les factures, valide la TVA, maintient un unique `rapprochement.json` par client (factures + lignes bancaires non rapprochées + anomalies + relances, groupés par période mensuelle). Le travail réel est fait par scripts/main.py (qui appelle scripts/extract.py). Ne traite jamais les e-mails, ne classe jamais de documents.
license: Interne — usage privé OpenClaw
---
# Skill `rapprochement-bancaire`
> Moteur d'état. Travaille uniquement sur l'arborescence `clients/<slug>/...` produite par `organisation-documents`.
> **Le travail réel est fait par `scripts/main.py`** (qui appelle `scripts/extract.py` pour toute l'extraction de texte). Ce skill = quand le lancer + comment rendre compte.
---
## ⚠️ Règle absolue — EXÉCUTION DU SCRIPT (non négociable)
**Pour chaque invocation, la SEULE action correcte est d'exécuter la commande :**
```bash
python3 scripts/main.py <racine_clients>
```
puis de lire le `rapprochement.json` de chaque client + le `compta_batch_report_<date>.json` consolidé, et de relayer au comptable.
### ❌ INTERDIT
- **Réimplémenter le rapprochement en Python inline / pseudo-code.** La logique (Pass 1 par référence, Pass 2 fuzzy, validation TVA, détection des anomalies, statuts overdue/partial, génération des relances, regroupement par période) est dans `scripts/main.py`. La refaire à la main est garanti de diverger.
- **Écrire `rapprochement.json` à la main.** C'est le script qui le fait, dans son format exact (objet `{ client, generated_at, periods[] }` ; chaque période contient `invoices[]`, `unmatched_bank_lines[]`, `period_anomalies[]`, `relances[]` ; chaque facture porte son champ `anomalies[]`). Toute autre forme (liste plate, dict keyé par `invoice_id`, champs `payments` / `amount_paid` ad hoc) casse les lectures ultérieures.
- **Modifier le format du fichier de sortie.** Le format est contractuel entre ce skill et ses consommateurs (backend de provisioning, agent OpenClaw, dashboards).
- **Recréer les anciens fichiers `followup.json` / `relances.json` / `anomalies.json`.** Ils ont été remplacés par le `rapprochement.json` unique. Le script les supprime à chaque run s'ils traînent.
### ✅ OBLIGATOIRE
1. Exécuter la commande shell ci-dessus.
2. Lire le fichier produit.
3. Relayer au comptable un tableau lisible par client (factures payées / en attente / partielles / overdue, relances, anomalies — en mettant en avant les **bloquantes**).
### Si le script échoue
Signale l'erreur exacte (stderr). Ne rejoue PAS le batch à la main.
> Pourquoi : on a déjà eu un run où l'agent a produit le fichier de sortie au mauvais format (dict avec clés `"in/N"`, champs `payments`/`amount_paid`/`amount_remaining` au lieu de `bank_matched`/`status`). Conséquence : aucun outil ni skill aval ne pouvait l'exploiter. Le script génère le bon format à tous les coups.
---
## Exécution
```bash
python3 scripts/main.py [<racine_clients>]
# racine_clients par défaut : ~/.openclaw/workspace/clients
```
À lancer :
- une fois par jour (idéalement la nuit), sur tous les clients ;
- ou immédiatement après un trigger `rapprochement-bancaire` émis par `organisation-documents`.
Le script :
1. **Parcourt** `clients/*` en ignorant les dossiers techniques `_*` (`_a-identifier`, `_incomplet`, `_non-attribue`, `_cabinet`).
2. **Périodes traitées** : mois courant + mois précédent, plus tout mois ancien non verrouillé (`batch.lock.json` absent). Un mois verrouillé n'est pas retraité.
3. **Factures** : nom de fichier conventionnel (`AAAA-MM-JJ_N°Facture_Contrepartie_MontantTTC.pdf`) → `invoice_id` + montant. Si le nom n'est pas exploitable, lecture du contenu via `extract.py`. Si toujours rien → anomalie de période `facture_illisible`.
4. **Relevés bancaires** : transactions extraites ligne par ligne via `extract.py` (`DATE | LIBELLÉ | MONTANT | CR/DB`), avec la référence facture si le libellé contient `REF <id>` ou `FACT <id>`. Aucune transaction extractible → anomalie de période `releve_non_parseable`.
5. **Rapprochement** (montant comparé en valeur absolue : une facture `out` est encaissée par un crédit, une `in` réglée par un débit) :
- **Pass 1** — réf facture : transaction dont `invoice_ref == facture.invoice_id`. Montant exact (±1 €) → `paid` ; montant inférieur → `partial` (conserve `amount_paid`, `amount_remaining`) ; supérieur → `paid` + `overpaid_by`.
- **Pass 2** — fuzzy : `|montant| ±1 €` ET similarité libellé / contrepartie ≥ 0.6.
- Aucun match + échéance dépassée → `overdue`.
6. **Validation TVA** de chaque facture : si `|TVA déclarée − TVA attendue| / TVA attendue > 5 %` (TVA attendue = `TOTAL HT × taux`) → anomalie `tva_incorrecte` rattachée à la facture (TVA 0 % / exonération ignorée).
7. **Anomalies** : voir tableau ci-dessous. Les anomalies *rattachables à une facture* (`tva_incorrecte`, `invoice_overdue`) vivent dans le champ `anomalies[]` de la facture. Les anomalies *non rattachables* (`doublon_paiement`, `releve_non_parseable`, `facture_illisible`) vont dans `period_anomalies[]` de leur période. Les lignes bancaires non rapprochées (`facture_manquante`, `paiement_orphelin`) vont dans `unmatched_bank_lines[]` de la période où la transaction a été observée.
8. **Relances** : `overdue` / `partial` / `unpaid` hors délai → step selon ancienneté, dans `relances[]` de la période de la facture.
9. Écrit `clients/<slug>/rapprochement.json` (un seul fichier par client) et un rapport consolidé `compta_batch_report_<date>.json`.
**Après l'exécution**, lire le fichier et relayer au comptable un tableau lisible par client (factures payées / en attente, relances, anomalies — en mettant en avant les anomalies **bloquantes**). Jamais de chemins techniques.
---
## Format de sortie (`rapprochement.json`)
```json
{
"client": "acme-sa",
"generated_at": "2026-05-20",
"periods": [
{
"period": "2026-05",
"locked": false,
"invoices": [
{
"invoice_id": "F-001",
"type": "out",
"amount": 1200.00,
"status": "paid",
"bank_matched": true,
"matched_tx": "VIR ACME REF F-001",
"issued_date": "2026-05-03",
"due_date": "2026-06-02",
"counterparty": "ACME",
"counterparty_name": "ACME SA",
"source_file": "clients/acme-sa/2026/05/invoices/out/...",
"anomalies": []
}
],
"unmatched_bank_lines": [
{
"type": "facture_manquante",
"invoice_ref": "F-099",
"label": "VIR REF F-099",
"amount": 2500,
"date": "2026-05-12",
"blocking": true
}
],
"period_anomalies": [
{ "type": "doublon_paiement", "label": "VIR ACME", "amount": 850, "date": "2026-05-09", "blocking": true }
],
"relances": [
{ "invoice_id": "F-007", "counterparty": "DUPONT",
"amount": 450, "due_date": "2026-04-02", "days_late": 48,
"step": 2, "status": "pending", "next_action_date": "2026-05-25" }
]
}
]
}
```
---
## Statuts de facture
| Statut | Sens |
|--------|------|
| `unpaid` | non échue, aucun paiement |
| `paid` | paiement confirmé (rapprochement réussi) |
| `partial` | paiement partiel — `amount_paid` + `amount_remaining` conservés |
| `overdue` | échéance dépassée, aucun paiement |
---
## Anomalies
**Bloquantes** (empêchent la clôture de la période) :
| Type | Où | Condition |
|------|----|-----------|
| `doublon_paiement` | `period_anomalies` | même date + montant + libellé |
| `tva_incorrecte` | `invoices[].anomalies` | écart TVA calculée / déclarée > 5 % |
| `facture_manquante` | `unmatched_bank_lines` | une ligne du relevé cite un n° de facture (`REF`/`FACT`) absent du dossier, montant > 1 000 € |
| `paiement_orphelin` | `unmatched_bank_lines` | crédit > 1 000 € sans aucune référence ni facture |
**Non bloquantes** (signalées, clôture possible) :
| Type | Où | Condition |
|------|----|-----------|
| `facture_manquante` | `unmatched_bank_lines` | n° de facture cité au relevé mais absent, montant ≤ 1 000 € |
| `paiement_orphelin` | `unmatched_bank_lines` | crédit ≤ 1 000 € sans référence |
| `releve_non_parseable` | `period_anomalies` | aucune transaction extractible d'un relevé |
| `facture_illisible` | `period_anomalies` | facture dont ni le nom ni le contenu ne donnent n° + montant |
| `invoice_overdue` | `invoices[].anomalies` | facture non payée, échéance dépassée |
> `facture_manquante` ≠ `paiement_orphelin` : le premier = paiement qui **cite** un n° de facture qu'on n'a pas reçu (le client a oublié de transmettre la pièce) ; le second = encaissement sans aucune référence.
---
## Relances (`periods[].relances`)
| Ancienneté du retard | Step |
|----------------------|------|
| ≤ 30 j | 1 |
| ≤ 60 j | 2 |
| ≤ 90 j | 3 |
| > 90 j | escalation |
Pour `partial` : mention explicite `"Solde restant dû : X,XX €"`.
---
## Clôture de période
Le script crée `clients/<slug>/<AAAA>/<MM>/batch.lock.json` quand : aucune anomalie **bloquante** sur la période, hash des fichiers stable depuis 7 jours, aucun statut `unpaid`/`overdue` non justifié. Les périodes verrouillées ne sont jamais retraitées sauf changement de hash.
---
## Règles critiques
- Ne jamais supprimer de données. Ne jamais retraiter un mois verrouillé.
- `rapprochement.json` est un **cache reconstructible** : les PDFs classés restent la vérité. Le batch peut tout recalculer depuis zéro.
- `clients.json` est lu seulement (maintenu par `organisation-documents`), jamais écrit ici.
- Toute l'extraction de texte passe par `scripts/extract.py` — source unique, pas de logique de parsing dupliquée ici.
- Le matching peut être inter-période : un paiement de mai peut solder une facture de janvier. La facture reste dans la période de son `issued_date` (status `paid`), la transaction n'apparaît pas en `unmatched_bank_lines` de mai.
---
## Philosophie
```
organisation-documents → classe les pièces, déduit clients.json
rapprochement-bancaire → rapproche, valide, reconstruit l'état comptable (scripts/main.py)
relances → décision différée
```
Le système doit pouvoir être recalculé intégralement à partir des documents classés.
don't have the plugin yet? install it then click "run inline in claude" again.