""" 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 try: with httpx.Client(base_url=base_url.rstrip("/"), headers=headers, timeout=30, 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 expiré ?)" data = r.json() return 200, data.get("transactions", []), None 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