Sync Indy integre dans l'interface web + anti-doublon
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user