Files
Thierry Tallagrand 1e3c0fc337 workspace-bilans: agent bilans AIRTRIA (fiche, outil, donnees), renvoi maison
- Nouvel agent « bilans » : AGENTS.md tire de reference-bilans.md (AIRTRIA_BILAN),
  scripts/bilans.py et bilans/airtria-balances.json identiques au pack
  /mnt/projects/analyse-bilans. Joignable dans le groupe WhatsApp « Bilans AIRTRIA »
  du compte Samsung (binding peer groupe dans openclaw.json, non versionne).
- Agent maison : §4 « MAILS », §4bis renvoi des bilans vers le groupe.
- .gitignore : liste blanche de workspace-bilans (memoire et etat exclus).
2026-10-03 00:47:02 +02:00

400 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
"""Analyse des bilans AIRTRIA — outil OpenClaw (python3 >= 3.11, bibliothèque standard seule).
Lit bilans/airtria-balances.json (balance par compte de chaque exercice, produite par
extract.py) et répond en texte français prêt à relayer tel quel.
bilans.py liste exercices disponibles, CA et résultat
bilans.py resume [AN] synthèse d'un exercice (défaut : le dernier)
bilans.py compare AN1 AN2 évolution des indicateurs entre deux exercices
bilans.py evolution [INDICATEUR] série sur tous les exercices (sans argument : liste)
bilans.py charges [AN] détail des charges par compte
bilans.py compte MOTIF [AN] compte(s) dont le numéro commence par / le libellé contient MOTIF
bilans.py ratios [AN] ratios avec leur formule
bilans.py sources provenance des chiffres et contrôles de cohérence
Option globale : --json (sortie brute pour débogage).
Exercice N = du 01/10/N-1 au 30/09/N.
"""
import json
import os
import sys
from pathlib import Path
DATA = Path(os.environ.get("BILANS_DATA", Path(__file__).resolve().parent.parent / "bilans/airtria-balances.json"))
# ── Chargement ────────────────────────────────────────────────────────────────
def charger():
try:
return json.loads(DATA.read_text(encoding="utf-8"))
except FileNotFoundError:
sortir(f"ERREUR : fichier de données introuvable : {DATA}", 2)
def sortir(msg, code=1):
print(msg)
sys.exit(code)
# ── Agrégats (Plan comptable général) ─────────────────────────────────────────
def somme(comptes, *prefixes, signe=1):
"""Somme des soldes (débit - crédit) des comptes commençant par l'un des préfixes.
signe=-1 pour les comptes à solde normalement créditeur (produits, passif)."""
return signe * sum(c["solde"] for n, c in comptes.items() if n.startswith(prefixes))
def somme_signee(comptes, prefixes, debiteur):
"""Somme des seuls comptes débiteurs (actif) ou créditeurs (passif), compte par compte :
un compte fournisseur débiteur est une créance, une banque créditrice une dette."""
tot = 0.0
for n, c in comptes.items():
if n.startswith(prefixes):
if debiteur and c["solde"] > 0:
tot += c["solde"]
elif not debiteur and c["solde"] < 0:
tot -= c["solde"]
return tot
def indicateurs(comptes):
s = lambda *p: somme(comptes, *p)
p = lambda *p: somme(comptes, *p, signe=-1)
i = {}
# Compte de résultat
i["ca"] = p("70")
i["autres_produits_exploit"] = p("71", "72", "74", "75", "781", "791")
i["achats_charges_externes"] = s("60", "61", "62")
i["impots_taxes"] = s("63")
i["remuneration_dirigeant"] = s("641")
i["charges_sociales"] = s("645", "646", "647", "648")
i["dotations_amortissements"] = s("681")
i["autres_charges_exploit"] = s("65")
i["resultat_exploitation"] = (i["ca"] + i["autres_produits_exploit"] - i["achats_charges_externes"]
- i["impots_taxes"] - s("64") - i["dotations_amortissements"]
- i["autres_charges_exploit"])
i["produits_financiers"] = p("76", "786", "796")
i["charges_financieres"] = s("66", "686")
i["resultat_financier"] = i["produits_financiers"] - i["charges_financieres"]
i["resultat_exceptionnel"] = p("77", "787", "797") - s("67", "687")
i["impot_societes"] = s("69")
i["resultat_net"] = p("6", "7")
# Soldes intermédiaires de gestion
i["valeur_ajoutee"] = i["ca"] + p("71", "72") - i["achats_charges_externes"]
i["ebe"] = i["valeur_ajoutee"] + p("74") - i["impots_taxes"] - s("64")
i["caf"] = (i["resultat_net"] + s("68") - p("78") + s("675") - p("775"))
# Bilan
i["immobilisations_brutes"] = s("20", "21", "22", "23", "26", "27")
i["amortissements_cumules"] = p("28", "29")
i["actif_immobilise_net"] = i["immobilisations_brutes"] - i["amortissements_cumules"]
i["creances"] = somme_signee(comptes, ("40", "41", "42", "43", "44", "45", "46", "47", "48"), True)
i["vmp"] = s("50")
i["disponibilites"] = somme_signee(comptes, ("51", "53"), True)
i["total_actif"] = i["actif_immobilise_net"] + i["creances"] + i["vmp"] + i["disponibilites"]
i["capitaux_propres"] = p("10", "11", "12", "13", "14") + i["resultat_net"]
i["emprunts"] = p("16") + somme_signee(comptes, ("51",), False)
i["dettes_fournisseurs"] = somme_signee(comptes, ("40",), False)
i["dettes_fiscales_sociales"] = somme_signee(comptes, ("42", "43", "44"), False)
i["compte_courant_associe"] = somme_signee(comptes, ("455",), False)
i["autres_dettes"] = somme_signee(comptes, ("41", "45", "46", "47", "48"), False) - i["compte_courant_associe"]
i["total_passif"] = (i["capitaux_propres"] + i["emprunts"] + i["dettes_fournisseurs"]
+ i["dettes_fiscales_sociales"] + i["compte_courant_associe"] + i["autres_dettes"])
i["tresorerie_nette"] = i["disponibilites"] + i["vmp"] - somme_signee(comptes, ("51",), False)
i["fonds_de_roulement"] = i["capitaux_propres"] + p("16") - i["actif_immobilise_net"]
i["bfr"] = i["creances"] - (i["dettes_fournisseurs"] + i["dettes_fiscales_sociales"]
+ i["compte_courant_associe"] + i["autres_dettes"])
return {k: round(v, 2) for k, v in i.items()}
def ratios(i):
def pct(a, b):
return None if not b else round(100 * a / b, 1)
return {
"marge_nette": (pct(i["resultat_net"], i["ca"]), "résultat net / CA"),
"taux_ebe": (pct(i["ebe"], i["ca"]), "EBE / CA"),
"taux_valeur_ajoutee": (pct(i["valeur_ajoutee"], i["ca"]), "valeur ajoutée / CA"),
"poids_charges_externes": (pct(i["achats_charges_externes"], i["ca"]), "achats et charges externes / CA"),
"poids_remuneration": (pct(i["remuneration_dirigeant"] + i["charges_sociales"], i["ca"]),
"(rémunération dirigeant + charges sociales) / CA"),
"autonomie_financiere": (pct(i["capitaux_propres"], i["total_passif"]), "capitaux propres / total bilan"),
"endettement": (pct(i["emprunts"], i["capitaux_propres"]), "emprunts et découverts / capitaux propres"),
"liquidite_immediate": (pct(i["disponibilites"] + i["vmp"],
i["dettes_fournisseurs"] + i["dettes_fiscales_sociales"] + i["autres_dettes"]),
"(disponibilités + VMP) / dettes à court terme hors compte courant"),
}
LIBELLES = {
"ca": "Chiffre d'affaires", "autres_produits_exploit": "Autres produits d'exploitation",
"achats_charges_externes": "Achats et charges externes", "impots_taxes": "Impôts et taxes",
"remuneration_dirigeant": "Rémunération du dirigeant", "charges_sociales": "Charges sociales",
"dotations_amortissements": "Dotations aux amortissements", "autres_charges_exploit": "Autres charges d'exploitation",
"resultat_exploitation": "Résultat d'exploitation", "produits_financiers": "Produits financiers",
"charges_financieres": "Charges financières", "resultat_financier": "Résultat financier",
"resultat_exceptionnel": "Résultat exceptionnel", "impot_societes": "Impôt sur les sociétés",
"resultat_net": "Résultat net", "valeur_ajoutee": "Valeur ajoutée", "ebe": "EBE",
"caf": "Capacité d'autofinancement", "immobilisations_brutes": "Immobilisations brutes",
"amortissements_cumules": "Amortissements cumulés", "actif_immobilise_net": "Actif immobilisé net",
"creances": "Créances", "vmp": "Valeurs mobilières de placement", "disponibilites": "Disponibilités",
"total_actif": "Total bilan", "capitaux_propres": "Capitaux propres", "emprunts": "Emprunts et découverts",
"dettes_fournisseurs": "Dettes fournisseurs", "dettes_fiscales_sociales": "Dettes fiscales et sociales",
"compte_courant_associe": "Compte courant d'associé", "autres_dettes": "Autres dettes",
"total_passif": "Total passif", "tresorerie_nette": "Trésorerie nette (dispo + VMP - découverts)",
"fonds_de_roulement": "Fonds de roulement", "bfr": "Besoin en fonds de roulement",
}
NOMS_RATIOS = {"marge_nette": "Marge nette", "taux_ebe": "Taux d'EBE", "taux_valeur_ajoutee": "Taux de valeur ajoutée",
"poids_charges_externes": "Poids des charges externes", "poids_remuneration": "Poids de la rémunération",
"autonomie_financiere": "Autonomie financière", "endettement": "Endettement",
"liquidite_immediate": "Liquidité immédiate"}
ALIAS = {"chiffre_affaires": "ca", "chiffre": "ca", "resultat": "resultat_net", "benefice": "resultat_net",
"tresorerie": "tresorerie_nette", "treso": "tresorerie_nette", "fr": "fonds_de_roulement",
"remuneration": "remuneration_dirigeant", "salaire": "remuneration_dirigeant",
"charges_externes": "achats_charges_externes", "cp": "capitaux_propres", "total_bilan": "total_actif",
"va": "valeur_ajoutee", "amortissements": "dotations_amortissements", "dettes": "emprunts"}
# ── Mise en forme ─────────────────────────────────────────────────────────────
def eur(v):
if v is None:
return "n.d."
n = round(v)
s = f"{abs(n):,}".replace(",", " ")
return f"{'-' if n < 0 else ''}{s} €"
def pc(v):
return "n.d." if v is None else f"{v:.1f} %".replace(".", ",")
def variation(a, b):
"""Évolution de a (ancien) vers b (récent)."""
d = b - a
sgn = "+" if d >= 0 else "-"
txt = f"{sgn}{eur(abs(d))}"
if a and abs(a) >= 1:
txt += f" ({sgn}{pc(abs(100 * d / a))})" if (a > 0) else ""
return txt
def exercice(data, an):
if an is None:
an = max(data["exercices"])
an = str(an)
if an not in data["exercices"]:
sortir(f"Exercice {an} absent. Exercices disponibles : {', '.join(data['exercices'])} "
f"(exercice N = du 01/10/N-1 au 30/09/N).")
return an, data["exercices"][an]
def entete(an, e):
return f"AIRTRIA — exercice {an} (du {fr_date(e['debut'])} au {fr_date(e['fin'])})"
def fr_date(iso):
y, m, d = iso.split("-")
return f"{d}/{m}/{y}"
# ── Commandes ─────────────────────────────────────────────────────────────────
def cmd_liste(data, args):
lignes = ["AIRTRIA — exercices disponibles (clôture au 30/09)", ""]
for an, e in data["exercices"].items():
i = indicateurs(e["comptes"])
lignes.append(f"{an} : CA {eur(i['ca'])}, résultat {eur(i['resultat_net'])}"
+ (" (*)" if "note" in e else ""))
if any("note" in e for e in data["exercices"].values()):
lignes += ["", "(*) remarque sur la source : voir « resume AN » ou « sources »."]
return "\n".join(lignes)
def cmd_resume(data, args):
an, e = exercice(data, args[0] if args else None)
i = indicateurs(e["comptes"])
r = ratios(i)
prec = data["exercices"].get(str(int(an) - 1))
ip = indicateurs(prec["comptes"]) if prec else None
vs = lambda k: f" ({variation(ip[k], i[k])} vs {int(an) - 1})" if ip else ""
L = [entete(an, e), "",
"ACTIVITÉ ET RÉSULTAT",
f"- Chiffre d'affaires : {eur(i['ca'])}{vs('ca')}",
f"- Achats et charges externes : {eur(i['achats_charges_externes'])}",
f"- Impôts et taxes : {eur(i['impots_taxes'])}",
f"- Rémunération dirigeant + charges sociales : {eur(i['remuneration_dirigeant'] + i['charges_sociales'])}",
f"- EBE : {eur(i['ebe'])} ({pc(r['taux_ebe'][0])} du CA)",
f"- Dotations aux amortissements : {eur(i['dotations_amortissements'])}",
f"- Résultat d'exploitation : {eur(i['resultat_exploitation'])}",
f"- Résultat financier : {eur(i['resultat_financier'])}",
]
if i["resultat_exceptionnel"]:
L.append(f"- Résultat exceptionnel : {eur(i['resultat_exceptionnel'])}")
if i["impot_societes"]:
L.append(f"- Impôt sur les sociétés : {eur(i['impot_societes'])}")
L += [f"- RÉSULTAT NET : {eur(i['resultat_net'])}{vs('resultat_net')}",
f"- Capacité d'autofinancement : {eur(i['caf'])}",
"",
"BILAN AU " + fr_date(e["fin"]),
f"- Actif immobilisé net : {eur(i['actif_immobilise_net'])}",
f"- Disponibilités : {eur(i['disponibilites'])} ; placements (VMP) : {eur(i['vmp'])}",
f"- Créances : {eur(i['creances'])}",
f"- Capitaux propres : {eur(i['capitaux_propres'])}",
f"- Emprunts et découverts : {eur(i['emprunts'])}",
f"- Dettes fournisseurs, fiscales et sociales : {eur(i['dettes_fournisseurs'] + i['dettes_fiscales_sociales'])}",
f"- Compte courant d'associé : {eur(i['compte_courant_associe'])}",
f"- Total bilan : {eur(i['total_actif'])}",
"",
f"Marge nette {pc(r['marge_nette'][0])} · autonomie financière {pc(r['autonomie_financiere'][0])}"
f" · trésorerie nette {eur(i['tresorerie_nette'])}",
]
off = e.get("officiel")
if off:
bits = [f"{lbl} {eur(off[k])}" for k, lbl in (("chiffre_affaires", "CA"), ("resultat_net", "résultat"),
("total_bilan", "total bilan")) if k in off]
L += ["", f"Chiffres officiels déposés : {', '.join(bits)}."]
if "note" in e:
L += ["", "Remarque : " + e["note"]]
return "\n".join(L)
def cmd_compare(data, args):
if len(args) != 2:
sortir("Usage : bilans.py compare AN1 AN2")
a1, e1 = exercice(data, min(args))
a2, e2 = exercice(data, max(args))
i1, i2 = indicateurs(e1["comptes"]), indicateurs(e2["comptes"])
cles = ["ca", "achats_charges_externes", "impots_taxes", "remuneration_dirigeant", "charges_sociales",
"ebe", "dotations_amortissements", "resultat_exploitation", "resultat_financier", "resultat_net",
"caf", "disponibilites", "vmp", "capitaux_propres", "emprunts", "total_actif"]
L = [f"AIRTRIA — comparaison des exercices {a1} et {a2}", ""]
for k in cles:
if i1[k] or i2[k]:
L.append(f"- {LIBELLES[k]} : {eur(i1[k])} → {eur(i2[k])} ({variation(i1[k], i2[k])})")
r1, r2 = ratios(i1), ratios(i2)
L += ["", f"Marge nette : {pc(r1['marge_nette'][0])} → {pc(r2['marge_nette'][0])}",
f"Autonomie financière : {pc(r1['autonomie_financiere'][0])} → {pc(r2['autonomie_financiere'][0])}"]
for a, e in ((a1, e1), (a2, e2)):
if "note" in e:
L.append(f"Remarque {a} : {e['note']}")
return "\n".join(L)
def cmd_evolution(data, args):
if not args:
return ("Indicateurs disponibles pour « evolution » :\n"
+ "\n".join(f"- {k} : {v}" for k, v in LIBELLES.items())
+ "\nAlias acceptés : " + ", ".join(sorted(ALIAS)))
k = args[0].lower().replace("-", "_").replace("'", "_")
k = ALIAS.get(k, k)
if k not in LIBELLES:
sortir(f"Indicateur « {args[0]} » inconnu. Lance « bilans.py evolution » pour la liste.")
serie = [(an, indicateurs(e["comptes"])[k]) for an, e in data["exercices"].items()]
L = [f"AIRTRIA — {LIBELLES[k]} par exercice", ""]
prev = None
for an, v in serie:
L.append(f"{an} : {eur(v)}" + (f" ({variation(prev, v)})" if prev is not None else ""))
prev = v
vals = [v for _, v in serie]
hi = max(serie, key=lambda x: x[1])
lo = min(serie, key=lambda x: x[1])
L += ["", f"Plus haut : {hi[0]} ({eur(hi[1])}) · plus bas : {lo[0]} ({eur(lo[1])})"
f" · moyenne : {eur(sum(vals) / len(vals))}"]
return "\n".join(L)
def cmd_charges(data, args):
an, e = exercice(data, args[0] if args else None)
ch = [(n, c["lib"], c["solde"]) for n, c in e["comptes"].items() if n.startswith("6") and c["solde"]]
ch.sort(key=lambda x: -x[2])
tot = sum(x[2] for x in ch)
ca = indicateurs(e["comptes"])["ca"]
L = [entete(an, e), f"Charges : {eur(tot)} au total ({pc(100 * tot / ca if ca else None)} du CA)", ""]
for n, lib, v in ch:
L.append(f"- {n} {lib.capitalize()} : {eur(v)} ({pc(100 * v / tot)})")
return "\n".join(L)
def cmd_compte(data, args):
if not args:
sortir("Usage : bilans.py compte MOTIF [AN] (MOTIF = début de numéro, ex. 626, ou mot du libellé, ex. assurance)")
motif = args[0].upper()
ans = [str(args[1])] if len(args) > 1 else list(data["exercices"])
for a in ans:
exercice(data, a)
trouves = {}
for a in ans:
for n, c in data["exercices"][a]["comptes"].items():
if n.startswith(motif) or motif in c["lib"].upper():
trouves.setdefault((n, c["lib"]), {})[a] = c
if not trouves:
return f"Aucun compte ne correspond à « {args[0]} » ({', '.join(ans) if len(ans) < 3 else 'tous exercices'})."
L = [f"AIRTRIA — comptes correspondant à « {args[0]} »", ""]
for (n, lib), par_an in sorted(trouves.items()):
L.append(f"{n} {lib}")
for a, c in par_an.items():
sens = "débiteur" if c["solde"] > 0 else "créditeur" if c["solde"] < 0 else "soldé"
L.append(f" {a} : solde {eur(abs(c['solde']))} {sens} (débit {eur(c['debit'])}, crédit {eur(c['credit'])})")
return "\n".join(L)
def cmd_ratios(data, args):
an, e = exercice(data, args[0] if args else None)
i = indicateurs(e["comptes"])
L = [entete(an, e), ""]
for k, (v, formule) in ratios(i).items():
L.append(f"- {NOMS_RATIOS[k]} : {pc(v)} [{formule}]")
L += [f"- Fonds de roulement : {eur(i['fonds_de_roulement'])} [capitaux propres + emprunts - actif immobilisé net]",
f"- BFR : {eur(i['bfr'])} [créances - dettes d'exploitation et compte courant]",
f"- Trésorerie nette : {eur(i['tresorerie_nette'])}"]
return "\n".join(L)
def cmd_sources(data, args):
L = [f"Données générées le {data['genere_le']} depuis /mnt/gestion/BILAN.",
data["societe"]["exercice"], ""]
for c in data["controles"]:
an = str(c["exercice"])
e = data["exercices"][an]
etat = []
if c["resultat_officiel"] is not None:
etat.append(f"liasse/PV {eur(c['resultat_officiel'])} {'OK' if c['ok_officiel'] else 'ÉCART'}")
if c["resultat_repris_n_plus_1"] is not None:
etat.append(f"repris en {int(an) + 1} : {eur(c['resultat_repris_n_plus_1'])}"
f" {'OK' if c['ok_report'] else 'ÉCART'}")
L.append(f"{an} : résultat {eur(c['resultat_calcule'])} — {'; '.join(etat) or 'pas de contrôle externe'}")
L.append(f" source : {e['source']}")
if "note" in e:
L.append(f" remarque : {e['note']}")
return "\n".join(L)
COMMANDES = {"liste": cmd_liste, "resume": cmd_resume, "compare": cmd_compare, "evolution": cmd_evolution,
"charges": cmd_charges, "compte": cmd_compte, "ratios": cmd_ratios, "sources": cmd_sources}
def main(argv):
as_json = "--json" in argv
argv = [a for a in argv if a != "--json"]
if not argv or argv[0] in ("-h", "--help", "aide"):
print(__doc__.strip())
return 0
cmd, args = argv[0].lower(), argv[1:]
if cmd not in COMMANDES:
sortir(f"Commande « {cmd} » inconnue. Commandes : {', '.join(COMMANDES)}.")
data = charger()
if as_json:
ans = [a for a in args if a.isdigit()] or list(data["exercices"])
out = {a: {"indicateurs": indicateurs(data["exercices"][a]["comptes"]),
"ratios": {k: v for k, (v, _) in ratios(indicateurs(data["exercices"][a]["comptes"])).items()}}
for a in ans if a in data["exercices"]}
print(json.dumps(out, ensure_ascii=False, indent=1))
return 0
print(COMMANDES[cmd](data, args))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))