169 lines
6.4 KiB
Python
169 lines
6.4 KiB
Python
"""
|
||
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)
|