Gestion des depenses ventilees (multi-taux TVA) a l'import Indy
Deploy via Portainer / deploy (push) Successful in 0s
Deploy via Portainer / deploy (push) Successful in 0s
This commit is contained in:
+70
-46
@@ -6,8 +6,15 @@ La « catégorie » Indy = un numéro de compte du plan comptable (PCG). On mapp
|
|||||||
numéro vers nos catégories via le catalogue (categories_indy). Montants en
|
numéro vers nos catégories via le catalogue (categories_indy). Montants en
|
||||||
centimes, signés. `tva_intracom` => UE (autoliquidation).
|
centimes, signés. `tva_intracom` => UE (autoliquidation).
|
||||||
|
|
||||||
API non officielle d'Indy : pour les données de l'utilisateur. Le token JWT
|
Ventilation : une transaction peut être éclatée en plusieurs subdivisions de
|
||||||
expire (~1 h) et doit être renouvelé.
|
charge (ex. un repas à 10 % + 20 %). Chaque subdivision de charge porte son HT
|
||||||
|
(`amount_in_cents`), son taux (`tva_rate`) et sa TVA (`tva_amount_in_cents`).
|
||||||
|
Indy ajoute aussi des subdivisions « miroir » de TVA (compte 445*, `is_tva:true`)
|
||||||
|
et la contrepartie bancaire (compte 512*) : on les ignore. On émet donc UNE ligne
|
||||||
|
par subdivision de charge, avec un identifiant de groupe commun (l'id de la
|
||||||
|
transaction) pour reconstituer l'opération d'origine.
|
||||||
|
|
||||||
|
API non officielle d'Indy : le token JWT expire (~1 h) et doit être renouvelé.
|
||||||
"""
|
"""
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
@@ -17,6 +24,7 @@ from .categories_indy import COMPTE_VERS_NOM, COMPTE_ACTIVITE
|
|||||||
|
|
||||||
BASE_URL = "https://app.indy.fr"
|
BASE_URL = "https://app.indy.fr"
|
||||||
TRANSACTIONS_PATH = "/api/transactions/transactions-list"
|
TRANSACTIONS_PATH = "/api/transactions/transactions-list"
|
||||||
|
CENT = Decimal("0.01")
|
||||||
|
|
||||||
|
|
||||||
def _cat(num):
|
def _cat(num):
|
||||||
@@ -27,80 +35,96 @@ def _act(num):
|
|||||||
return COMPTE_ACTIVITE.get(str(num or ""), "")
|
return COMPTE_ACTIVITE.get(str(num or ""), "")
|
||||||
|
|
||||||
|
|
||||||
def normaliser(tx):
|
def _charge_subdivisions(tx):
|
||||||
"""Transaction Indy -> ligne normalisée, ou None si inexploitable."""
|
"""Subdivisions de charge/produit réelles : hors banque (512*) et hors
|
||||||
try:
|
miroir de TVA (445* avec is_tva)."""
|
||||||
montant = (Decimal(tx["totalAmountInCents"]) / 100).quantize(Decimal("0.01"))
|
out = []
|
||||||
except Exception:
|
for s in tx.get("subdivisions", []):
|
||||||
return None
|
num = str((s.get("accounting_account") or {}).get("number", ""))
|
||||||
subs = [s for s in tx.get("subdivisions", [])
|
if num.startswith("512"):
|
||||||
if not str((s.get("accounting_account") or {}).get("number", "")).startswith("512")]
|
continue
|
||||||
|
if s.get("is_tva"):
|
||||||
|
continue
|
||||||
|
out.append((num, s))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def lignes_transaction(tx):
|
||||||
|
"""Transaction Indy -> liste de lignes normalisées (une par subdivision de
|
||||||
|
charge). Liste vide si inexploitable."""
|
||||||
|
txid = tx.get("_id", "")
|
||||||
|
date = tx.get("date", "")
|
||||||
|
libelle = tx.get("description") or tx.get("raw_description") or "(sans libellé)"
|
||||||
|
subs = _charge_subdivisions(tx)
|
||||||
if not subs:
|
if not subs:
|
||||||
return None
|
return []
|
||||||
main = max(subs, key=lambda s: abs(s.get("amount_in_cents", 0)))
|
# Mode de saisie de la TVA : en "tva_ttc" le montant inclut déjà la TVA.
|
||||||
num = str((main.get("accounting_account") or {}).get("number", ""))
|
mode = tx.get("tva_selected") or (tx.get("vat") or {}).get("tvaSelected") or "tva_ht"
|
||||||
intra = main.get("tva_intracom")
|
inclut_tva = mode == "tva_ttc"
|
||||||
|
n = len(subs)
|
||||||
|
lignes = []
|
||||||
|
for i, (num, s) in enumerate(subs):
|
||||||
|
amt = Decimal(s.get("amount_in_cents", 0))
|
||||||
|
tva = Decimal(s.get("tva_amount_in_cents", 0))
|
||||||
|
ttc = (amt if inclut_tva else amt + tva) / 100
|
||||||
|
ttc = ttc.quantize(CENT)
|
||||||
|
intra = s.get("tva_intracom")
|
||||||
if intra and intra.get("type") == "eu":
|
if intra and intra.get("type") == "eu":
|
||||||
regime, taux = "intracom", Decimal(intra.get("rate", 0)) / 100
|
regime, taux = "intracom", Decimal(intra.get("rate", 0)) / 100
|
||||||
else:
|
else:
|
||||||
regime, taux = "nationale", Decimal(main.get("tva_rate", 0)) / 100
|
regime, taux = "nationale", Decimal(s.get("tva_rate", 0)) / 100
|
||||||
return {
|
lignes.append({
|
||||||
"indy_id": tx.get("_id", ""),
|
"indy_id": f"{txid}#{i}" if n > 1 else txid,
|
||||||
"date": tx.get("date", ""),
|
"groupe": txid,
|
||||||
"libelle": tx.get("description") or tx.get("raw_description") or "(sans libellé)",
|
"n_parts": n,
|
||||||
|
"date": date,
|
||||||
|
"libelle": libelle,
|
||||||
"categorie_nom": _cat(num),
|
"categorie_nom": _cat(num),
|
||||||
"activite": _act(num),
|
"activite": _act(num),
|
||||||
"taux": taux,
|
"taux": taux,
|
||||||
"regime": regime,
|
"regime": regime,
|
||||||
"montant": montant,
|
"montant": ttc,
|
||||||
"compte": num,
|
"compte": num,
|
||||||
}
|
})
|
||||||
|
return lignes
|
||||||
|
|
||||||
|
|
||||||
|
def normaliser(tx):
|
||||||
|
"""Compat : première ligne de charge de la transaction (ou None)."""
|
||||||
|
lignes = lignes_transaction(tx)
|
||||||
|
return lignes[0] if lignes else None
|
||||||
|
|
||||||
|
|
||||||
def fetch_transactions(token, date_debut=None, date_fin=None, search="", page=1,
|
def fetch_transactions(token, date_debut=None, date_fin=None, search="", page=1,
|
||||||
base_url=BASE_URL):
|
base_url=BASE_URL):
|
||||||
"""Appelle l'API Indy. Retourne (status, liste_transactions_brutes, erreur)."""
|
headers = {"Accept": "application/json", "Authorization": f"Bearer {token}",
|
||||||
headers = {
|
"x-client-app-type": "web"}
|
||||||
"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}
|
params = {"search": search or "", "page": page}
|
||||||
if date_debut:
|
if date_debut:
|
||||||
params["dateFrom"] = date_debut
|
params["dateFrom"] = date_debut
|
||||||
if date_fin:
|
if date_fin:
|
||||||
params["dateTo"] = date_fin
|
params["dateTo"] = date_fin
|
||||||
timeout = httpx.Timeout(12.0, connect=8.0)
|
|
||||||
try:
|
try:
|
||||||
with httpx.Client(base_url=base_url.rstrip("/"), headers=headers,
|
with httpx.Client(base_url=base_url.rstrip("/"), headers=headers,
|
||||||
timeout=timeout, follow_redirects=True) as c:
|
timeout=httpx.Timeout(12.0, connect=8.0), follow_redirects=True) as c:
|
||||||
r = c.get(TRANSACTIONS_PATH, params=params)
|
r = c.get(TRANSACTIONS_PATH, params=params)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
return r.status_code, [], (
|
return r.status_code, [], f"HTTP {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():
|
if "json" not in r.headers.get("content-type", "").lower():
|
||||||
return r.status_code, [], (
|
return r.status_code, [], "Reponse non-JSON (Cloudflare)."
|
||||||
"Reponse non-JSON (probablement Cloudflare). Le conteneur n'arrive "
|
return 200, r.json().get("transactions", []), None
|
||||||
"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:
|
except httpx.TimeoutException:
|
||||||
return None, [], ("Delai depasse en joignant Indy : le conteneur n'a peut-etre "
|
return None, [], "Delai depasse."
|
||||||
"pas d'acces Internet, ou Cloudflare bloque. Utilise le favori "
|
except Exception as ex:
|
||||||
"depuis ton poste.")
|
|
||||||
except Exception as ex: # noqa: BLE001
|
|
||||||
return None, [], f"{type(ex).__name__}: {ex}"
|
return None, [], f"{type(ex).__name__}: {ex}"
|
||||||
|
|
||||||
|
|
||||||
def lignes_normalisees(token, date_debut=None, date_fin=None, search="", page=1,
|
def lignes_normalisees(token, date_debut=None, date_fin=None, search="", page=1,
|
||||||
base_url=BASE_URL):
|
base_url=BASE_URL):
|
||||||
"""Retourne (lignes_normalisées, erreur)."""
|
|
||||||
status, txs, err = fetch_transactions(token, date_debut, date_fin, search, page, base_url)
|
status, txs, err = fetch_transactions(token, date_debut, date_fin, search, page, base_url)
|
||||||
if err:
|
if err:
|
||||||
return [], err
|
return [], err
|
||||||
lignes = [n for n in (normaliser(t) for t in txs) if n]
|
lignes = []
|
||||||
|
for t in txs:
|
||||||
|
lignes.extend(lignes_transaction(t))
|
||||||
return lignes, None
|
return lignes, None
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
.tag.depense { color:var(--warn); border-color:#7a5f1f; }
|
.tag.depense { color:var(--warn); border-color:#7a5f1f; }
|
||||||
.tag.operation { color:var(--accent); border-color:#25566e; }
|
.tag.operation { color:var(--accent); border-color:#25566e; }
|
||||||
.neutre-badge { font-size:10px; color:var(--muted); border:1px dashed var(--line); border-radius:6px; padding:1px 5px; margin-left:6px; }
|
.neutre-badge { font-size:10px; color:var(--muted); border:1px dashed var(--line); border-radius:6px; padding:1px 5px; margin-left:6px; }
|
||||||
|
.ventile-badge { font-size:10px; color:var(--accent); border:1px solid #25566e; border-radius:6px; padding:1px 5px; margin-left:6px; }
|
||||||
.btn-mini { padding:5px 11px; font-size:12.5px; border-radius:8px; }
|
.btn-mini { padding:5px 11px; font-size:12.5px; border-radius:8px; }
|
||||||
#gtable td, #gtable th { border-bottom:1px solid var(--line); }
|
#gtable td, #gtable th { border-bottom:1px solid var(--line); }
|
||||||
#gtable tbody tr:hover td { background:#243247; }
|
#gtable tbody tr:hover td { background:#243247; }
|
||||||
@@ -105,7 +106,7 @@
|
|||||||
<td>{{ r.date|date:"d/m/Y" }}</td>
|
<td>{{ r.date|date:"d/m/Y" }}</td>
|
||||||
<td><span class="tag {{ r.type }}">{{ r.type_label }}</span></td>
|
<td><span class="tag {{ r.type }}">{{ r.type_label }}</span></td>
|
||||||
<td>{{ r.libelle }}</td>
|
<td>{{ r.libelle }}</td>
|
||||||
<td>{{ r.categorie }}{% if r.neutre %}<span class="neutre-badge">neutre</span>{% endif %}</td>
|
<td>{{ r.categorie }}{% if r.neutre %}<span class="neutre-badge">neutre</span>{% endif %}{% if r.ventile %}<span class="ventile-badge" title="Part d'une dépense ventilée (plusieurs taux de TVA)">ventilé</span>{% endif %}</td>
|
||||||
<td class="{% if r.montant >= 0 %}pos{% else %}neg{% endif %}">{{ r.montant|floatformat:2|unlocalize }} €</td>
|
<td class="{% if r.montant >= 0 %}pos{% else %}neg{% endif %}">{{ r.montant|floatformat:2|unlocalize }} €</td>
|
||||||
<td>{% if r.tva %}{{ r.tva|floatformat:2|unlocalize }}{% else %}—{% endif %}</td>
|
<td>{% if r.tva %}{{ r.tva|floatformat:2|unlocalize }}{% else %}—{% endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
<td>{{ l.type }}</td>
|
<td>{{ l.type }}</td>
|
||||||
<input type="hidden" name="l_activite" value="{{ l.activite }}">
|
<input type="hidden" name="l_activite" value="{{ l.activite }}">
|
||||||
<input type="hidden" name="l_indy_id" value="{{ l.indy_id }}">
|
<input type="hidden" name="l_indy_id" value="{{ l.indy_id }}">
|
||||||
|
<input type="hidden" name="l_groupe" value="{{ l.groupe }}">
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db.models import Count
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@@ -106,8 +107,14 @@ def _rows(entreprise):
|
|||||||
"regime": d.regime_tva, "taux": d.taux_tva,
|
"regime": d.regime_tva, "taux": d.taux_tva,
|
||||||
"montant": -d.montant_ttc, "tva": d.tva_deductible_value,
|
"montant": -d.montant_ttc, "tva": d.tva_deductible_value,
|
||||||
"neutre": d.neutre, "signe": Decimal("-1"), "editable_montant": True,
|
"neutre": d.neutre, "signe": Decimal("-1"), "editable_montant": True,
|
||||||
|
"ventile": bool(d.paiement_id),
|
||||||
})
|
})
|
||||||
for p in Paiement.objects.filter(entreprise=entreprise).select_related("categorie"):
|
# Opérations bancaires SANS dépenses rattachées : les « conteneurs » d'une
|
||||||
|
# ventilation sont représentés par leurs dépenses (évite le double comptage).
|
||||||
|
paiements = (Paiement.objects.filter(entreprise=entreprise)
|
||||||
|
.select_related("categorie")
|
||||||
|
.annotate(nb_dep=Count("depenses")).filter(nb_dep=0))
|
||||||
|
for p in paiements:
|
||||||
rows.append({
|
rows.append({
|
||||||
"type": "operation", "type_label": "Opération", "id": p.pk,
|
"type": "operation", "type_label": "Opération", "id": p.pk,
|
||||||
"date": p.date, "libelle": p.libelle,
|
"date": p.date, "libelle": p.libelle,
|
||||||
|
|||||||
+64
-12
@@ -78,16 +78,18 @@ def _match_cat(text, cats):
|
|||||||
|
|
||||||
|
|
||||||
def _row(dt, libelle, montant, taux=Decimal("0"), regime=RegimeTva.NATIONALE,
|
def _row(dt, libelle, montant, taux=Decimal("0"), regime=RegimeTva.NATIONALE,
|
||||||
categorie=None, activite="", indy_id="", n=0):
|
categorie=None, activite="", indy_id="", n=0, groupe="", n_parts=1):
|
||||||
neutre = bool(categorie and categorie.exclure_calculs)
|
neutre = bool(categorie and categorie.exclure_calculs)
|
||||||
if montant > 0:
|
if montant > 0:
|
||||||
typ = "Mouvement neutre" if neutre else "Vente"
|
typ = "Mouvement neutre" if neutre else "Vente"
|
||||||
else:
|
else:
|
||||||
typ = "Dépense neutre" if neutre else "Dépense"
|
typ = "Dépense neutre" if neutre else "Dépense"
|
||||||
|
if n_parts > 1:
|
||||||
|
typ += " (ventilée)"
|
||||||
return {"n": n, "date": dt.isoformat() if dt else "", "libelle": libelle,
|
return {"n": n, "date": dt.isoformat() if dt else "", "libelle": libelle,
|
||||||
"categorie_id": categorie.pk if categorie else "",
|
"categorie_id": categorie.pk if categorie else "",
|
||||||
"taux": taux, "regime": regime, "montant": montant, "type": typ,
|
"taux": taux, "regime": regime, "montant": montant, "type": typ,
|
||||||
"activite": activite, "indy_id": indy_id}
|
"activite": activite, "indy_id": indy_id, "groupe": groupe}
|
||||||
|
|
||||||
|
|
||||||
# ---- CSV ----------------------------------------------------------------
|
# ---- CSV ----------------------------------------------------------------
|
||||||
@@ -248,7 +250,7 @@ def _parse_indy(entreprise, token, date_debut, date_fin, cats_par_nom):
|
|||||||
cat = cats_par_nom.get((ln["categorie_nom"] or "").strip().lower())
|
cat = cats_par_nom.get((ln["categorie_nom"] or "").strip().lower())
|
||||||
lignes.append(_row(dt, ln["libelle"], ln["montant"], ln["taux"],
|
lignes.append(_row(dt, ln["libelle"], ln["montant"], ln["taux"],
|
||||||
_regime(ln["regime"]), cat, ln["activite"] or "",
|
_regime(ln["regime"]), cat, ln["activite"] or "",
|
||||||
ln["indy_id"], n))
|
ln["indy_id"], n, ln.get("groupe", ""), ln.get("n_parts", 1)))
|
||||||
return lignes, []
|
return lignes, []
|
||||||
|
|
||||||
|
|
||||||
@@ -262,6 +264,17 @@ def _deja_importe(entreprise, indy_id):
|
|||||||
or Paiement.objects.filter(entreprise=entreprise, indy_id=indy_id).exists())
|
or Paiement.objects.filter(entreprise=entreprise, indy_id=indy_id).exists())
|
||||||
|
|
||||||
|
|
||||||
|
def _deja_importe_groupe(entreprise, groupe):
|
||||||
|
"""Vrai si la transaction Indy (ou une de ses parts ventilées) est déjà là."""
|
||||||
|
if not groupe:
|
||||||
|
return False
|
||||||
|
from django.db.models import Q
|
||||||
|
q = Q(indy_id=groupe) | Q(indy_id__startswith=f"{groupe}#")
|
||||||
|
return (Facture.objects.filter(entreprise=entreprise).filter(q).exists()
|
||||||
|
or Depense.objects.filter(entreprise=entreprise).filter(q).exists()
|
||||||
|
or Paiement.objects.filter(entreprise=entreprise).filter(q).exists())
|
||||||
|
|
||||||
|
|
||||||
# ---- Création -----------------------------------------------------------
|
# ---- Création -----------------------------------------------------------
|
||||||
|
|
||||||
def _creer(entreprise, dt, libelle, montant, taux, regime, categorie,
|
def _creer(entreprise, dt, libelle, montant, taux, regime, categorie,
|
||||||
@@ -295,6 +308,29 @@ def _creer(entreprise, dt, libelle, montant, taux, regime, categorie,
|
|||||||
return "depense"
|
return "depense"
|
||||||
|
|
||||||
|
|
||||||
|
def _creer_groupe(entreprise, rows):
|
||||||
|
"""rows = lignes d'une même transaction. Si ≥2 dépenses -> une opération
|
||||||
|
bancaire (le total) qui porte les dépenses ventilées. Sinon création simple."""
|
||||||
|
depenses = [r for r in rows if r["montant"] < 0]
|
||||||
|
if len(rows) >= 2 and len(depenses) == len(rows):
|
||||||
|
total = sum(-r["montant"] for r in rows)
|
||||||
|
p = Paiement.objects.create(
|
||||||
|
entreprise=entreprise, date=rows[0]["dt"], libelle=rows[0]["libelle"],
|
||||||
|
montant_total=total, sens="debit", indy_id=rows[0].get("groupe", ""))
|
||||||
|
for r in rows:
|
||||||
|
Depense.objects.create(
|
||||||
|
entreprise=entreprise, paiement=p, date=r["dt"], libelle=r["libelle"],
|
||||||
|
categorie=r["categorie"], activite=r["activite"], regime_tva=r["regime"],
|
||||||
|
taux_tva=r["taux"], montant_ttc=-r["montant"], indy_id=r["indy_id"])
|
||||||
|
return {"dépense (ventilée)": len(rows)}
|
||||||
|
compteur = {}
|
||||||
|
for r in rows:
|
||||||
|
k = _creer(entreprise, r["dt"], r["libelle"], r["montant"], r["taux"],
|
||||||
|
r["regime"], r["categorie"], r["activite"], r["indy_id"])
|
||||||
|
compteur[k] = compteur.get(k, 0) + 1
|
||||||
|
return compteur
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def import_csv(request):
|
def import_csv(request):
|
||||||
entreprises = Entreprise.accessibles(request.user)
|
entreprises = Entreprise.accessibles(request.user)
|
||||||
@@ -323,24 +359,40 @@ def import_csv(request):
|
|||||||
montants = request.POST.getlist("l_montant")
|
montants = request.POST.getlist("l_montant")
|
||||||
activites = request.POST.getlist("l_activite")
|
activites = request.POST.getlist("l_activite")
|
||||||
indy_ids = request.POST.getlist("l_indy_id")
|
indy_ids = request.POST.getlist("l_indy_id")
|
||||||
|
groupes = request.POST.getlist("l_groupe")
|
||||||
cats_by_id = {str(c.pk): c for c in Categorie.objects.all()}
|
cats_by_id = {str(c.pk): c for c in Categorie.objects.all()}
|
||||||
compteur = {}
|
compteur = {}
|
||||||
ignores = 0
|
ignores = 0
|
||||||
|
# 1) Construire les lignes valides, en préservant l'ordre des groupes.
|
||||||
|
groupes_ordre = []
|
||||||
|
par_groupe = {}
|
||||||
for i in range(len(libelles)):
|
for i in range(len(libelles)):
|
||||||
dt = _date(dates[i]) if i < len(dates) else None
|
dt = _date(dates[i]) if i < len(dates) else None
|
||||||
montant = _dec(montants[i] if i < len(montants) else "0")
|
montant = _dec(montants[i] if i < len(montants) else "0")
|
||||||
iid = indy_ids[i] if i < len(indy_ids) else ""
|
|
||||||
if dt is None or montant == 0:
|
if dt is None or montant == 0:
|
||||||
continue
|
continue
|
||||||
if iid and _deja_importe(entreprise, iid):
|
iid = indy_ids[i] if i < len(indy_ids) else ""
|
||||||
ignores += 1
|
grp = (groupes[i] if i < len(groupes) else "") or ""
|
||||||
|
cle = grp or f"_solo_{i}" # lignes sans groupe = isolées
|
||||||
|
if cle not in par_groupe:
|
||||||
|
par_groupe[cle] = []
|
||||||
|
groupes_ordre.append((cle, grp))
|
||||||
|
par_groupe[cle].append({
|
||||||
|
"dt": dt, "montant": montant,
|
||||||
|
"libelle": (libelles[i] or "").strip() or "(sans libellé)",
|
||||||
|
"taux": _dec(taux[i] if i < len(taux) else "0"),
|
||||||
|
"regime": _regime(regimes[i] if i < len(regimes) else ""),
|
||||||
|
"categorie": cats_by_id.get(cat_ids[i] if i < len(cat_ids) else ""),
|
||||||
|
"activite": _activite(activites[i] if i < len(activites) else ""),
|
||||||
|
"indy_id": iid, "groupe": grp})
|
||||||
|
# 2) Créer groupe par groupe (anti-doublon par transaction).
|
||||||
|
for cle, grp in groupes_ordre:
|
||||||
|
rows = par_groupe[cle]
|
||||||
|
if grp and _deja_importe_groupe(entreprise, grp):
|
||||||
|
ignores += len(rows)
|
||||||
continue
|
continue
|
||||||
k = _creer(entreprise, dt, (libelles[i] or "").strip() or "(sans libellé)",
|
for k, v in _creer_groupe(entreprise, rows).items():
|
||||||
montant, _dec(taux[i] if i < len(taux) else "0"),
|
compteur[k] = compteur.get(k, 0) + v
|
||||||
_regime(regimes[i] if i < len(regimes) else ""),
|
|
||||||
cats_by_id.get(cat_ids[i] if i < len(cat_ids) else ""),
|
|
||||||
_activite(activites[i] if i < len(activites) else ""), iid)
|
|
||||||
compteur[k] = compteur.get(k, 0) + 1
|
|
||||||
if ignores:
|
if ignores:
|
||||||
compteur["déjà importées (ignorées)"] = ignores
|
compteur["déjà importées (ignorées)"] = ignores
|
||||||
ctx["resultat"] = compteur
|
ctx["resultat"] = compteur
|
||||||
|
|||||||
Reference in New Issue
Block a user