0a4ba2001b
modified: .env.example new file: compta/__pycache__/__init__.cpython-310.pyc new file: compta/__pycache__/admin.cpython-310.pyc new file: compta/__pycache__/apps.cpython-310.pyc new file: compta/__pycache__/models.cpython-310.pyc modified: compta/admin.py new file: compta/categories_indy.py modified: compta/indy.py modified: compta/management/commands/seed_reference.py new file: compta/migrations/0010_categorie_compte_pcg.py modified: compta/models.py modified: compta/templates/compta/base.html new file: compta/templates/compta/gestion.html modified: compta/urls.py new file: compta/views_gestion.py new file: config/__pycache__/__init__.cpython-310.pyc new file: config/__pycache__/settings.cpython-310.pyc modified: config/settings.py modified: entrypoint.sh
107 lines
4.2 KiB
Python
107 lines
4.2 KiB
Python
"""
|
|
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
|