Rapprochement Bancaire

Other

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.

Install

openclaw skills install rapprochement-bancaire

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 :

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

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)

{
  "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

StatutSens
unpaidnon échue, aucun paiement
paidpaiement confirmé (rapprochement réussi)
partialpaiement 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) :

TypeCondition
doublon_paiementperiod_anomaliesmême date + montant + libellé
tva_incorrecteinvoices[].anomaliesécart TVA calculée / déclarée > 5 %
facture_manquanteunmatched_bank_linesune ligne du relevé cite un n° de facture (REF/FACT) absent du dossier, montant > 1 000 €
paiement_orphelinunmatched_bank_linescrédit > 1 000 € sans aucune référence ni facture

Non bloquantes (signalées, clôture possible) :

TypeCondition
facture_manquanteunmatched_bank_linesn° de facture cité au relevé mais absent, montant ≤ 1 000 €
paiement_orphelinunmatched_bank_linescrédit ≤ 1 000 € sans référence
releve_non_parseableperiod_anomaliesaucune transaction extractible d'un relevé
facture_illisibleperiod_anomaliesfacture dont ni le nom ni le contenu ne donnent n° + montant
invoice_overdueinvoices[].anomaliesfacture non payée, échéance dépassée

facture_manquantepaiement_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 retardStep
≤ 30 j1
≤ 60 j2
≤ 90 j3
> 90 jescalation

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.