Files
jb-compta/compta/indy.py
T

131 lines
5.1 KiB
Python

"""
Synchronisation Indy : récupère les transactions via l'API privée d'Indy et les
normalise au format attendu par l'import (réutilisé par la page /import/).
La « catégorie » Indy est un numéro de compte du plan comptable (PCG) ; on le
mappe vers nos catégories. Montants en centimes, signés. `tva_intracom` => UE.
API non officielle d'Indy : pour les données de l'utilisateur. Le token JWT
expire (~1 h) et doit être renouvelé.
"""
from decimal import Decimal
import httpx
BASE_URL = "https://app.indy.fr"
TRANSACTIONS_PATH = "/api/transactions/transactions-list"
COMPTE_CATEGORIE = [
("701", "Vente de produits finis"), ("707", "Vente de produits finis"),
("706", "Prestation de services"),
("605", "Matériel et outillage"), ("606", "Matériel et outillage"),
("607", "Matériel et outillage"),
("6256", "Frais de repas hors domicile"), ("6253", "Frais de repas hors domicile"),
("626", "Télécom, fournitures, documentation"),
("6181", "Télécom, fournitures, documentation"),
("6183", "Télécom, fournitures, documentation"),
("6136", "Abonnement logiciel"), ("6226", "Abonnement logiciel"),
("646", "Cotisation sociale Urssaf"),
("44551", "TVA payée"), ("44558", "TVA payée"),
("44583", "Remboursement de TVA"),
("108", "Prélèvement personnel"),
]
COMPTE_ACTIVITE = [("701", "vente"), ("707", "vente"), ("706", "service_bic")]
def _cat(num):
num = str(num or "")
best = None
for pref, cat in COMPTE_CATEGORIE:
if num.startswith(pref) and (best is None or len(pref) > len(best[0])):
best = (pref, cat)
return best[1] if best else None
def _act(num):
num = str(num or "")
for pref, act in COMPTE_ACTIVITE:
if num.startswith(pref):
return act
return ""
def normaliser(tx):
"""Transaction Indy -> ligne normalisée, ou None si inexploitable."""
try:
montant = (Decimal(tx["totalAmountInCents"]) / 100).quantize(Decimal("0.01"))
except Exception:
return None
subs = [s for s in tx.get("subdivisions", [])
if not str((s.get("accounting_account") or {}).get("number", "")).startswith("512")]
if not subs:
return None
main = max(subs, key=lambda s: abs(s.get("amount_in_cents", 0)))
num = str((main.get("accounting_account") or {}).get("number", ""))
intra = main.get("tva_intracom")
if intra and intra.get("type") == "eu":
regime, taux = "intracom", Decimal(intra.get("rate", 0)) / 100
else:
regime, taux = "nationale", Decimal(main.get("tva_rate", 0)) / 100
return {
"indy_id": tx.get("_id", ""),
"date": tx.get("date", ""),
"libelle": tx.get("description") or tx.get("raw_description") or "(sans libellé)",
"categorie_nom": _cat(num),
"activite": _act(num),
"taux": taux,
"regime": regime,
"montant": montant,
"compte": num,
}
def fetch_transactions(token, date_debut=None, date_fin=None, search="", page=1,
base_url=BASE_URL):
"""Appelle l'API Indy. Retourne (status, liste_transactions_brutes, erreur)."""
headers = {
"Accept": "application/json, text/plain, */*",
"Authorization": f"Bearer {token}",
"User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/149.0 Safari/537.36"),
"x-client-app-type": "web",
}
params = {"search": search or "", "page": page}
if date_debut:
params["dateFrom"] = date_debut
if date_fin:
params["dateTo"] = date_fin
timeout = httpx.Timeout(12.0, connect=8.0)
try:
with httpx.Client(base_url=base_url.rstrip("/"), headers=headers,
timeout=timeout, follow_redirects=True) as c:
r = c.get(TRANSACTIONS_PATH, params=params)
if r.status_code != 200:
return r.status_code, [], (
f"HTTP {r.status_code} — token expire, ou requete bloquee par "
f"Cloudflare depuis le serveur.")
if "json" not in r.headers.get("content-type", "").lower():
return r.status_code, [], (
"Reponse non-JSON (probablement Cloudflare). Le conteneur n'arrive "
"pas a joindre Indy directement. Recupere le CSV depuis ton poste "
"(MCP/CLI) et colle-le ci-dessous.")
data = r.json()
return 200, data.get("transactions", []), None
except httpx.TimeoutException:
return None, [], ("Delai depasse en joignant Indy : le conteneur n'a peut-etre "
"pas d'acces Internet, ou Cloudflare bloque. Utilise le CSV "
"depuis ton poste.")
except Exception as ex: # noqa: BLE001
return None, [], f"{type(ex).__name__}: {ex}"
def lignes_normalisees(token, date_debut=None, date_fin=None, search="", page=1,
base_url=BASE_URL):
"""Retourne (lignes_normalisées, erreur)."""
status, txs, err = fetch_transactions(token, date_debut, date_fin, search, page, base_url)
if err:
return [], err
lignes = [n for n in (normaliser(t) for t in txs) if n]
return lignes, None