Groupe Gestionnaire + import CSV des operations

This commit is contained in:
2026-06-30 08:40:53 +02:00
parent 688fa3fc97
commit ab8af8d4bb
21 changed files with 297 additions and 5 deletions
+168
View File
@@ -0,0 +1,168 @@
"""
Import d'un relevé d'opérations au format CSV.
Colonnes : date ; libelle ; categorie ; activite ; taux_tva ; regime ; montant
- montant négatif = dépense, positif = vente (ou neutre si catégorie neutre).
Aperçu avant enregistrement.
"""
import csv
import io
from decimal import Decimal, InvalidOperation
from datetime import datetime
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from .models import (Entreprise, Client, Facture, LigneFacture, Paiement,
Depense, Categorie, Activite, RegimeTva)
COLONNES = ["date", "libelle", "categorie", "activite", "taux_tva", "regime", "montant"]
CENT = Decimal("0.01")
def _dec(v, d="0"):
try:
return Decimal(str(v or d).replace(",", ".").replace(" ", "").strip() or d)
except (InvalidOperation, AttributeError):
return Decimal(d)
def _date(s):
s = (s or "").strip()
for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y"):
try:
return datetime.strptime(s, fmt).date()
except ValueError:
continue
return None
def _activite(s):
s = (s or "").strip().lower()
table = {"vente": "vente", "service_bic": "service_bic", "service_bnc": "service_bnc",
"bic": "service_bic", "bnc": "service_bnc"}
return table.get(s, "")
def _regime(s):
s = (s or "").strip().lower()
if s in ("intracom", "intracommunautaire", "ue"):
return RegimeTva.INTRACOM
if s in ("import", "hors ue"):
return RegimeTva.IMPORT
return RegimeTva.NATIONALE
def _parse(text, cats):
"""Retourne (lignes, erreurs). Chaque ligne = dict prêt à créer + aperçu."""
text = text.lstrip("")
delim = ";" if text.count(";") >= text.count(",") else ","
reader = csv.reader(io.StringIO(text), delimiter=delim)
rows = list(reader)
if not rows:
return [], ["Fichier vide."]
# En-tête optionnel
start = 1 if rows and rows[0] and rows[0][0].strip().lower() in ("date", "date ") else 0
lignes, erreurs = [], []
for n, raw in enumerate(rows[start:], start=start + 1):
if not any(c.strip() for c in raw):
continue
c = (raw + [""] * 7)[:7]
dt = _date(c[0])
montant = _dec(c[6])
if dt is None:
erreurs.append(f"Ligne {n} : date invalide « {c[0]} ».")
continue
if montant == 0:
erreurs.append(f"Ligne {n} : montant nul.")
continue
cat = cats.get((c[2] or "").strip().lower())
taux = _dec(c[4])
regime = _regime(c[5])
neutre = bool(cat and cat.exclure_calculs)
if montant > 0:
typ = "Mouvement neutre" if neutre else "Vente"
else:
typ = "Dépense neutre" if neutre else "Dépense"
lignes.append({
"n": n, "date": dt, "libelle": (c[1] or "").strip() or "(sans libellé)",
"categorie": cat, "categorie_nom": (c[2] or "").strip(),
"activite": _activite(c[3]), "taux": taux, "regime": regime,
"montant": montant, "neutre": neutre, "type": typ,
})
return lignes, erreurs
def _creer(entreprise, l):
montant = l["montant"]
if montant > 0:
if l["neutre"]:
Paiement.objects.create(entreprise=entreprise, date=l["date"],
libelle=l["libelle"], montant_total=montant,
sens="credit", categorie=l["categorie"])
return "vente_neutre"
client, _ = Client.objects.get_or_create(entreprise=entreprise, nom=l["libelle"])
ttc = montant
ht = (ttc / (Decimal("1") + l["taux"] / Decimal("100"))).quantize(CENT)
annee = l["date"].year
base = Facture.objects.filter(entreprise=entreprise,
numero__startswith=f"{annee}-").count() + 1
numero = f"{annee}-{base:03d}"
while Facture.objects.filter(entreprise=entreprise, numero=numero).exists():
base += 1
numero = f"{annee}-{base:03d}"
f = Facture.objects.create(
entreprise=entreprise, client=client, numero=numero,
activite=l["activite"] or entreprise.activite, date_emission=l["date"],
date_encaissement=l["date"], statut="encaissee")
LigneFacture.objects.create(facture=f, designation=l["libelle"],
quantite=Decimal("1"), prix_unitaire_ht=ht,
taux_tva=l["taux"])
f.recompute_totaux()
return "vente"
# Dépense
abs_m = -montant
if l["regime"] in (RegimeTva.INTRACOM, RegimeTva.IMPORT):
ttc, tva = abs_m, Decimal("0")
elif l["neutre"]:
ttc, tva = abs_m, Decimal("0")
else:
ttc = abs_m
tva = (abs_m * l["taux"] / (Decimal("100") + l["taux"])).quantize(CENT) if l["taux"] else Decimal("0")
Depense.objects.create(
entreprise=entreprise, date=l["date"], libelle=l["libelle"],
categorie=l["categorie"], activite=l["activite"], regime_tva=l["regime"],
taux_tva=l["taux"] if l["regime"] != RegimeTva.NATIONALE else Decimal("0"),
montant_ttc=ttc, montant_tva=tva)
return "depense"
@login_required
def import_csv(request):
entreprises = Entreprise.accessibles(request.user)
cats = {c.nom.strip().lower(): c for c in Categorie.objects.all()}
ctx = {"entreprises": entreprises, "colonnes": COLONNES}
if request.method == "POST":
entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first()
texte = request.POST.get("texte", "")
fichier = request.FILES.get("fichier")
if fichier:
texte = fichier.read().decode("utf-8-sig", "ignore")
lignes, erreurs = _parse(texte, cats)
ctx.update({"lignes": lignes, "erreurs": erreurs, "texte": texte,
"entreprise_id": int(request.POST.get("entreprise") or 0)})
if request.POST.get("action") == "import":
if not entreprise:
erreurs.append("Sélectionne une entreprise.")
if not lignes:
erreurs.append("Aucune ligne valide à importer.")
if not erreurs:
compteur = {}
for l in lignes:
k = _creer(entreprise, l)
compteur[k] = compteur.get(k, 0) + 1
ctx["resultat"] = compteur
ctx["lignes"] = None # import fait
ctx["erreurs"] = erreurs
return render(request, "compta/import_csv.html", ctx)