Files
jb-compta/compta/views_import.py
T

169 lines
6.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)