diff --git a/compta/__pycache__/indy.cpython-310.pyc b/compta/__pycache__/indy.cpython-310.pyc
new file mode 100644
index 0000000..d403d9c
Binary files /dev/null and b/compta/__pycache__/indy.cpython-310.pyc differ
diff --git a/compta/__pycache__/views_import.cpython-310.pyc b/compta/__pycache__/views_import.cpython-310.pyc
new file mode 100644
index 0000000..c93eb67
Binary files /dev/null and b/compta/__pycache__/views_import.cpython-310.pyc differ
diff --git a/compta/indy.py b/compta/indy.py
index 8f7da5c..44498f7 100644
--- a/compta/indy.py
+++ b/compta/indy.py
@@ -5,7 +5,7 @@ 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
+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
@@ -96,14 +96,26 @@ def fetch_transactions(token, date_debut=None, date_fin=None, search="", page=1,
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=30, follow_redirects=True) as c:
+ 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 expiré ?)"
+ 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}"
diff --git a/compta/templates/compta/import_csv.html b/compta/templates/compta/import_csv.html
index 33b7ea3..8bf24fe 100644
--- a/compta/templates/compta/import_csv.html
+++ b/compta/templates/compta/import_csv.html
@@ -1,4 +1,4 @@
-
+{% load l10n %}
@@ -128,8 +128,8 @@
{% for val,lab in regimes %}{% endfor %}
-
Un clic sur la page Indy → tes transactions sont copiées au format CSV,
+ prêtes à coller dans JB Compta. Pas de token à chercher, la 2FA est déjà passée,
+ et tout se fait dans ton navigateur (donc pas de souci d'accès serveur).
+
+
Installation (30 s)
+
+
Affiche ta barre de favoris (Ctrl+Maj+B dans Chrome).
+
Glisse-dépose ce bouton dans la barre de favoris :
+ Indy → CSV
+
Va sur app.indy.fr (connecté), puis clique le favori.
+
Saisis la période → le CSV est copié → colle-le dans /import/ de JB Compta.
+
+
+
Si le glisser-déposer ne marche pas : crée un favori manuellement,
+ puis colle le code ci-dessous comme URL du favori.
+
+
Code (installation manuelle)
+
+
+
Notes
+
+
À utiliser uniquement quand l'onglet actif est app.indy.fr.
+
Aucun secret n'est stocké : le favori lit ta session en cours.
+
Si une alerte « Erreur Indy » apparaît, reconnecte-toi à Indy et réessaie.
+
Le mapping des catégories (compte comptable → catégorie) est le même que dans l'app.
+
+
+
+
+
diff --git a/indy-mcp/bookmarklet.js b/indy-mcp/bookmarklet.js
new file mode 100644
index 0000000..b229020
--- /dev/null
+++ b/indy-mcp/bookmarklet.js
@@ -0,0 +1,69 @@
+/*
+ * Bookmarklet « Indy → CSV » — version lisible (documentation).
+ * Le bouton prêt à installer est dans bookmarklet.html.
+ *
+ * Usage : être connecté sur app.indy.fr, cliquer le favori. Il demande une
+ * période, récupère les transactions (dans TON navigateur = autorisé, 2FA déjà
+ * passée, pas de problème Cloudflare ni d'accès serveur), construit le CSV au
+ * format JB Compta et le copie dans le presse-papier. Tu le colles dans /import/.
+ */
+(async () => {
+ const CC = [
+ ["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"],
+ ];
+ const CA = [["701", "vente"], ["707", "vente"], ["706", "service_bic"]];
+ const cat = (n) => { n = String(n || ""); let b = null; for (const [p, c] of CC) { if (n.startsWith(p) && (!b || p.length > b[0].length)) b = [p, c]; } return b ? b[1] : ""; };
+ const act = (n) => { n = String(n || ""); for (const [p, a] of CA) { if (n.startsWith(p)) return a; } return ""; };
+ const norm = (tx) => {
+ const c = tx.totalAmountInCents; if (c == null) return null;
+ const mt = (c / 100).toFixed(2);
+ const ss = (tx.subdivisions || []).filter(s => !String((s.accounting_account || {}).number || "").startsWith("512"));
+ if (!ss.length) return null;
+ let m = ss[0]; for (const s of ss) if (Math.abs(s.amount_in_cents || 0) > Math.abs(m.amount_in_cents || 0)) m = s;
+ const num = String((m.accounting_account || {}).number || "");
+ let rg = "", tx2 = 0;
+ if (m.tva_intracom && m.tva_intracom.type === "eu") { rg = "intracom"; tx2 = (m.tva_intracom.rate || 0) / 100; }
+ else { tx2 = (m.tva_rate || 0) / 100; }
+ const lib = (tx.description || tx.raw_description || "").replace(/;/g, " ");
+ return [tx.date || "", lib, cat(num), act(num), String(tx2), rg, mt].join(";");
+ };
+ const getTok = () => new Promise(r => {
+ const o = indexedDB.open("firebaseLocalStorageDb");
+ o.onsuccess = () => { try {
+ const q = o.result.transaction("firebaseLocalStorage", "readonly").objectStore("firebaseLocalStorage").getAll();
+ q.onsuccess = () => { const v = (q.result || []).map(x => x.value).find(v => v && v.stsTokenManager && v.stsTokenManager.accessToken); r(v ? v.stsTokenManager.accessToken : null); };
+ q.onerror = () => r(null);
+ } catch (e) { r(null); } };
+ o.onerror = () => r(null);
+ });
+ const d1 = prompt("Date début (AAAA-MM-JJ)"); if (!d1) return;
+ const d2 = prompt("Date fin (AAAA-MM-JJ)"); if (!d2) return;
+ const tk = await getTok();
+ const H = { "Accept": "application/json", "x-client-app-type": "web" };
+ if (tk) H["Authorization"] = "Bearer " + tk;
+ let all = [], pg = 1, tot = 1e9;
+ try {
+ while (all.length < tot) {
+ const u = "/api/transactions/transactions-list?" + new URLSearchParams({ search: "", dateFrom: d1, dateTo: d2, page: pg });
+ const r = await fetch(u, { headers: H, credentials: "include" });
+ if (!r.ok) throw new Error("HTTP " + r.status);
+ const j = await r.json();
+ tot = j.nbTransactions != null ? j.nbTransactions : (j.transactions || []).length;
+ const t = j.transactions || []; if (!t.length) break;
+ all = all.concat(t); pg++; if (pg > 60) break;
+ }
+ } catch (e) { alert("Erreur Indy : " + e.message + "\nEs-tu bien connecté sur app.indy.fr ?"); return; }
+ const rows = all.map(norm).filter(Boolean);
+ const csv = "date;libelle;categorie;activite;taux_tva;regime;montant\n" + rows.join("\n");
+ try { await navigator.clipboard.writeText(csv); alert(rows.length + " opérations -> CSV copié ! Colle-le dans JB Compta (/import/)."); }
+ catch (e) { window.prompt("Copie ce CSV (Ctrl+C) :", csv); }
+})();