""" Synchronisation Indy : récupère les transactions via l'API privée d'Indy et les normalise au format d'import. La « catégorie » Indy = un numéro de compte du plan comptable (PCG). On mappe ce numéro vers nos catégories via le catalogue (categories_indy). Montants en centimes, signés. `tva_intracom` => UE (autoliquidation). 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 from .categories_indy import COMPTE_VERS_NOM, COMPTE_ACTIVITE BASE_URL = "https://app.indy.fr" TRANSACTIONS_PATH = "/api/transactions/transactions-list" def _cat(num): return COMPTE_VERS_NOM.get(str(num or "")) def _act(num): return COMPTE_ACTIVITE.get(str(num or ""), "") 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 " "(favori/MCP) 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 favori " "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