OCR pytesseract dans l'import + apercu editable

This commit is contained in:
2026-06-30 08:52:45 +02:00
parent ab8af8d4bb
commit 60aab53227
4 changed files with 287 additions and 133 deletions
+6 -2
View File
@@ -5,13 +5,17 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
# psycopg2-binary embarque sa propre libpq : pas besoin de build-essential ni libpq-dev. # Tesseract (OCR) + langue française, pour l'import par capture d'écran.
RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr tesseract-ocr-fra \
&& rm -rf /var/lib/apt/lists/*
# psycopg2-binary embarque sa propre libpq : pas besoin de build-essential.
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
# Fichiers statiques de l'admin (ne nécessite pas la base de données)
RUN DJANGO_SECRET_KEY=build python manage.py collectstatic --noinput RUN DJANGO_SECRET_KEY=build python manage.py collectstatic --noinput
EXPOSE 8000 EXPOSE 8000
+65 -46
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>JB Compta — Import CSV</title> <title>JB Compta — Import</title>
<style> <style>
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8; :root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
--accent:#38bdf8; --ok:#34d399; --bad:#f87171; } --accent:#38bdf8; --ok:#34d399; --bad:#f87171; }
@@ -12,84 +12,103 @@
background:var(--bg); color:var(--txt); padding:24px; } background:var(--bg); color:var(--txt); padding:24px; }
h1 { margin:0 0 4px; font-size:22px; } h2{font-size:15px;color:var(--muted);} h1 { margin:0 0 4px; font-size:22px; } h2{font-size:15px;color:var(--muted);}
a { color:var(--accent); } a { color:var(--accent); }
.sub { color:var(--muted); margin-bottom:16px; } .sub { color:var(--muted); margin-bottom:14px; }
.card { background:var(--card); border:1px solid #334155; border-radius:12px; padding:16px; margin-bottom:16px; } .card { background:var(--card); border:1px solid #334155; border-radius:12px; padding:16px; margin-bottom:16px; }
label { display:block; font-size:12px; color:var(--muted); margin:8px 0 4px; } label { display:block; font-size:12px; color:var(--muted); margin:8px 0 4px; }
input, select, textarea { background:#0f172a; color:var(--txt); border:1px solid #334155; input, select, textarea { background:#0f172a; color:var(--txt); border:1px solid #334155;
border-radius:8px; padding:8px 10px; font-size:14px; width:100%; } border-radius:8px; padding:8px 10px; font-size:14px; width:100%; }
textarea { min-height:140px; font-family:ui-monospace,monospace; } textarea { min-height:110px; font-family:ui-monospace,monospace; }
.grid3 { display:grid; grid-template-columns:2fr 1fr 1fr; gap:12px; }
@media(max-width:700px){ .grid3{grid-template-columns:1fr;} }
button { cursor:pointer; border-radius:8px; border:1px solid #334155; background:var(--card); button { cursor:pointer; border-radius:8px; border:1px solid #334155; background:var(--card);
color:var(--txt); padding:9px 16px; font-size:14px; } color:var(--txt); padding:9px 16px; font-size:14px; }
button.primary { background:var(--accent); color:#04263a; border:none; font-weight:600; } button.primary { background:var(--accent); color:#04263a; border:none; font-weight:600; }
.actions { display:flex; gap:12px; margin-top:12px; } .actions { display:flex; gap:12px; margin-top:12px; }
table { width:100%; border-collapse:collapse; font-size:13px; } table { width:100%; border-collapse:collapse; font-size:13px; }
th,td { padding:6px 8px; border-bottom:1px solid #334155; text-align:right; } th,td { padding:4px 6px; border-bottom:1px solid #334155; text-align:left; }
th:first-child,td:first-child,th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3) { text-align:left; } td input, td select { padding:5px 7px; }
.num { text-align:right; }
code { background:#0f172a; padding:1px 6px; border-radius:5px; } code { background:#0f172a; padding:1px 6px; border-radius:5px; }
.msg { padding:10px 14px; border-radius:8px; margin-bottom:8px; } .msg { padding:10px 14px; border-radius:8px; margin-bottom:8px; }
.msg.bad { background:#7f1d1d; } .msg.ok { background:#064e3b; } .msg.bad { background:#7f1d1d; } .msg.ok { background:#064e3b; }
.pill { padding:1px 8px; border-radius:99px; font-size:12px; background:#334155; }
</style> </style>
</head> </head>
<body> <body>
<h1>Import d'un relevé (CSV)</h1> <h1>Importer un relevé</h1>
<div class="sub"><a href="/dashboard/">← Tableau de bord</a> · <a href="/saisie/">Saisie manuelle</a></div> <div class="sub"><a href="/dashboard/">← Tableau de bord</a> · <a href="/saisie/">Saisie manuelle</a></div>
<div class="card">
<h2>Format attendu</h2>
<div class="sub">Colonnes séparées par <code>;</code> (en-tête facultatif) :</div>
<code>date ; libelle ; categorie ; activite ; taux_tva ; regime ; montant</code>
<div class="sub" style="margin-top:8px">
<b>montant</b> négatif = dépense, positif = vente · <b>regime</b> : nationale / intracom / import (vide = nationale)
· <b>activite</b> : vente / service_bic / service_bnc · une catégorie « neutre » (TVA payée, Urssaf, remboursement) n'entre pas dans les calculs.<br>
Exemple : <code>2026-06-25;ALTISSIMO;Vente de produits finis;vente;20;;168.40</code>
</div>
</div>
{% if erreurs %}{% for e in erreurs %}<div class="msg bad">{{ e }}</div>{% endfor %}{% endif %} {% if erreurs %}{% for e in erreurs %}<div class="msg bad">{{ e }}</div>{% endfor %}{% endif %}
{% if resultat %} {% if resultat %}
<div class="msg ok">Import terminé : <div class="msg ok">Import terminé :
{% for k, v in resultat.items %}{{ v }} {{ k }}{% if not forloop.last %} · {% endif %}{% endfor %}. {% for k, v in resultat.items %}{{ v }} {{ k }}{% if not forloop.last %} · {% endif %}{% endfor %}.
Va voir dans <a href="/admin/compta/depense/">les dépenses</a> et Voir <a href="/admin/compta/depense/">dépenses</a> · <a href="/admin/compta/facture/">factures</a>.</div>
<a href="/admin/compta/facture/">les factures</a>.
</div>
{% endif %} {% endif %}
{% if not lignes %}
<form method="post" enctype="multipart/form-data" class="card"> <form method="post" enctype="multipart/form-data" class="card">
{% csrf_token %} {% csrf_token %}
<label>Entreprise</label> <div class="grid3">
<select name="entreprise" required> <div>
{% for e in entreprises %}<option value="{{ e.pk }}" {% if e.pk == entreprise_id %}selected{% endif %}>{{ e.nom }}</option>{% endfor %} <label>Entreprise</label>
</select> <select name="entreprise" required>
<label>Fichier CSV</label> {% for e in entreprises %}<option value="{{ e.pk }}" {% if e.pk == entreprise_id %}selected{% endif %}>{{ e.nom }}</option>{% endfor %}
</select>
</div>
<div>
<label>Année (pour l'OCR)</label>
<input type="number" name="annee" value="{{ annee_defaut }}">
</div>
<div>
<label>Capture d'écran (OCR)</label>
<input type="file" name="image" accept="image/*">
</div>
</div>
<label>…ou fichier CSV</label>
<input type="file" name="fichier" accept=".csv,.txt"> <input type="file" name="fichier" accept=".csv,.txt">
<label>…ou colle directement les lignes ici</label> <label>…ou colle des lignes CSV <span class="sub">(date ; libelle ; categorie ; activite ; taux_tva ; regime ; montant)</span></label>
<textarea name="texte" placeholder="2026-06-25;ALTISSIMO;Vente de produits finis;vente;20;;168.40">{{ texte }}</textarea> <textarea name="texte" placeholder="2026-06-25;ALTISSIMO;Vente de produits finis;vente;20;;168.40">{{ texte }}</textarea>
<div class="actions"> <div class="actions">
<button type="submit" name="action" value="preview">Aperçu</button> <button type="submit" name="action" value="preview" class="primary">Analyser / Aperçu</button>
<button type="submit" name="action" value="import" class="primary">Importer</button>
</div> </div>
<div class="sub" style="margin-top:8px">L'OCR pré-remplit les lignes ; tu corriges la catégorie et le taux dans l'aperçu avant d'enregistrer.</div>
</form> </form>
{% endif %}
{% if lignes %} {% if lignes %}
<div class="card"> <form method="post" class="card">
<h2>Aperçu — {{ lignes|length }} ligne(s)</h2> {% csrf_token %}
<table> <input type="hidden" name="entreprise" value="{{ entreprise_id }}">
<thead><tr><th>#</th><th>Date</th><th>Libellé</th><th>Catégorie</th><th>Type</th><th>Taux</th><th>Régime</th><th>Montant</th></tr></thead> <h2>Aperçu éditable — {{ lignes|length }} ligne(s) {% if source == 'ocr' %}(OCR){% endif %}</h2>
<tbody> <table>
{% for l in lignes %} <thead><tr><th>Date</th><th>Libellé</th><th>Catégorie</th><th>Régime TVA</th><th>Taux %</th><th>Montant €</th><th>Type</th></tr></thead>
<tr> <tbody>
<td>{{ l.n }}</td><td>{{ l.date }}</td><td>{{ l.libelle }}</td> {% for l in lignes %}
<td>{{ l.categorie_nom }}{% if l.categorie_nom and not l.categorie %} ⚠️{% endif %}</td> <tr>
<td><span class="pill">{{ l.type }}</span></td> <td><input type="date" name="l_date" value="{{ l.date }}" style="width:140px"></td>
<td>{{ l.taux }} %</td><td>{{ l.regime }}</td><td>{{ l.montant }} €</td> <td><input type="text" name="l_libelle" value="{{ l.libelle }}"></td>
</tr> <td>
{% endfor %} <select name="l_categorie">
</tbody> <option value=""></option>
</table> {% for c in categories %}<option value="{{ c.pk }}" {% if c.pk == l.categorie_id %}selected{% endif %}>{{ c.nom }}</option>{% endfor %}
<div class="sub" style="margin-top:8px">⚠️ = catégorie non reconnue (sera laissée vide). Vérifie, puis clique « Importer ».</div> </select>
</td>
<td>
<select name="l_regime">
{% for val,lab in regimes %}<option value="{{ val }}" {% if val == l.regime %}selected{% endif %}>{{ lab }}</option>{% endfor %}
</select>
</td>
<td><input type="number" step="0.01" name="l_taux" value="{{ l.taux }}" class="num" style="width:70px"></td>
<td><input type="number" step="0.01" name="l_montant" value="{{ l.montant }}" class="num" style="width:100px"></td>
<td>{{ l.type }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="actions">
<button type="submit" name="action" value="save" class="primary">Enregistrer ces opérations</button>
<a href="/import/"><button type="button">Recommencer</button></a>
</div> </div>
</form>
{% endif %} {% endif %}
</body> </body>
</html> </html>
+214 -85
View File
@@ -1,13 +1,13 @@
""" """
Import d'un relevé d'opérations au format CSV. Import d'opérations : par CSV (collé/uploadé) ou par OCR d'une capture d'écran
Colonnes : date ; libelle ; categorie ; activite ; taux_tva ; regime ; montant (pytesseract). Dans les deux cas, un aperçu ÉDITABLE permet de corriger avant
- montant négatif = dépense, positif = vente (ou neutre si catégorie neutre). d'enregistrer (l'OCR n'est jamais parfait).
Aperçu avant enregistrement.
""" """
import csv import csv
import io import io
import re
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from datetime import datetime from datetime import datetime, date as date_cls
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.shortcuts import render from django.shortcuts import render
@@ -15,13 +15,20 @@ from django.shortcuts import render
from .models import (Entreprise, Client, Facture, LigneFacture, Paiement, from .models import (Entreprise, Client, Facture, LigneFacture, Paiement,
Depense, Categorie, Activite, RegimeTva) Depense, Categorie, Activite, RegimeTva)
COLONNES = ["date", "libelle", "categorie", "activite", "taux_tva", "regime", "montant"]
CENT = Decimal("0.01") CENT = Decimal("0.01")
MOIS = {"janvier": 1, "février": 2, "fevrier": 2, "mars": 3, "avril": 4, "mai": 5,
"juin": 6, "juillet": 7, "août": 8, "aout": 8, "septembre": 9,
"octobre": 10, "novembre": 11, "décembre": 12, "decembre": 12}
AMOUNT_RE = re.compile(r"^-?\d[\d  .]*[.,]\d{2}$")
def _dec(v, d="0"): def _dec(v, d="0"):
try: try:
return Decimal(str(v or d).replace(",", ".").replace(" ", "").strip() or d) s = str(v or d)
for ch in (" ", "", " "):
s = s.replace(ch, "")
s = s.replace(",", ".").strip()
return Decimal(s or d)
except (InvalidOperation, AttributeError): except (InvalidOperation, AttributeError):
return Decimal(d) return Decimal(d)
@@ -38,9 +45,8 @@ def _date(s):
def _activite(s): def _activite(s):
s = (s or "").strip().lower() s = (s or "").strip().lower()
table = {"vente": "vente", "service_bic": "service_bic", "service_bnc": "service_bnc", return {"vente": "vente", "service_bic": "service_bic", "service_bnc": "service_bnc",
"bic": "service_bic", "bnc": "service_bnc"} "bic": "service_bic", "bnc": "service_bnc"}.get(s, "")
return table.get(s, "")
def _regime(s): def _regime(s):
@@ -52,17 +58,36 @@ def _regime(s):
return RegimeTva.NATIONALE return RegimeTva.NATIONALE
def _parse(text, cats): def _match_cat(text, cats):
"""Retourne (lignes, erreurs). Chaque ligne = dict prêt à créer + aperçu.""" low = (text or "").lower()
for nom_low, c in cats.items():
if nom_low and nom_low in low:
return c
return None
def _row(dt, libelle, montant, taux=Decimal("0"), regime=RegimeTva.NATIONALE,
categorie=None, n=0):
neutre = bool(categorie and categorie.exclure_calculs)
if montant > 0:
typ = "Mouvement neutre" if neutre else "Vente"
else:
typ = "Dépense neutre" if neutre else "Dépense"
return {
"n": n, "date": dt.isoformat() if dt else "", "libelle": libelle,
"categorie_id": categorie.pk if categorie else "",
"taux": taux, "regime": regime, "montant": montant, "type": typ,
}
# ---- Parsing CSV --------------------------------------------------------
def _parse_csv(text, cats):
text = text.lstrip("") text = text.lstrip("")
delim = ";" if text.count(";") >= text.count(",") else "," delim = ";" if text.count(";") >= text.count(",") else ","
reader = csv.reader(io.StringIO(text), delimiter=delim) rows = list(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 = [], [] lignes, erreurs = [], []
start = 1 if rows and rows[0] and rows[0][0].strip().lower() == "date" else 0
for n, raw in enumerate(rows[start:], start=start + 1): for n, raw in enumerate(rows[start:], start=start + 1):
if not any(c.strip() for c in raw): if not any(c.strip() for c in raw):
continue continue
@@ -70,69 +95,145 @@ def _parse(text, cats):
dt = _date(c[0]) dt = _date(c[0])
montant = _dec(c[6]) montant = _dec(c[6])
if dt is None: if dt is None:
erreurs.append(f"Ligne {n} : date invalide « {c[0]} ».") erreurs.append(f"Ligne {n} : date invalide « {c[0]} »."); continue
continue
if montant == 0: if montant == 0:
erreurs.append(f"Ligne {n} : montant nul.") erreurs.append(f"Ligne {n} : montant nul."); continue
continue
cat = cats.get((c[2] or "").strip().lower()) cat = cats.get((c[2] or "").strip().lower())
taux = _dec(c[4]) lignes.append(_row(dt, (c[1] or "").strip() or "(sans libellé)", montant,
regime = _regime(c[5]) _dec(c[4]), _regime(c[5]), cat, n))
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 return lignes, erreurs
def _creer(entreprise, l): # ---- Parsing OCR --------------------------------------------------------
montant = l["montant"]
def _ocr_lang():
try:
import pytesseract
return "fra" if "fra" in pytesseract.get_languages(config="") else None
except Exception:
return None
def _parse_ocr(image_file, annee, cats):
try:
import pytesseract
from pytesseract import Output
from PIL import Image
except Exception:
return [], ["OCR indisponible (pytesseract / Pillow non installés)."]
try:
img = Image.open(image_file)
except Exception:
return [], ["Image illisible."]
if img.width < 1000:
ratio = 1400 / img.width
img = img.resize((int(img.width * ratio), int(img.height * ratio)))
kwargs = {"output_type": Output.DICT}
lang = _ocr_lang()
if lang:
kwargs["lang"] = lang
try:
data = pytesseract.image_to_data(img, **kwargs)
except Exception as ex:
return [], [f"OCR impossible ({ex}). Vérifie l'installation de Tesseract, "
f"ou utilise l'import CSV."]
words = []
for i in range(len(data["text"])):
t = data["text"][i].strip()
try:
conf = int(float(data["conf"][i]))
except (ValueError, TypeError):
conf = -1
if t and conf > 20:
words.append((data["top"][i], data["left"][i], t))
words.sort()
visual_rows, cur, cur_top = [], [], None
for top, left, t in words:
if cur_top is None or abs(top - cur_top) <= 18:
cur.append((left, t))
cur_top = top if cur_top is None else cur_top
else:
visual_rows.append(cur); cur = [(left, t)]; cur_top = top
if cur:
visual_rows.append(cur)
lignes = []
for n, row in enumerate(visual_rows, start=1):
toks = [t for _, t in sorted(row)]
dt = None
for i, t in enumerate(toks):
if t.isdigit() and i + 1 < len(toks):
m = MOIS.get(toks[i + 1].lower().strip(".,"))
if m and 1 <= int(t) <= 31:
dt = date_cls(annee, m, int(t)); break
montant, amount_idx = None, None
for i in range(len(toks) - 1, -1, -1):
if "%" not in toks[i] and AMOUNT_RE.match(toks[i]):
montant = _dec(toks[i]); amount_idx = i; break
taux = Decimal("0")
for t in toks:
if "%" in t:
taux = _dec(t.replace("%", ""))
if dt is None or montant is None or montant == 0:
continue
reste, skip = [], False
for i, t in enumerate(toks):
if i == amount_idx or "%" in t or t.upper() == "TVA":
continue
if t.isdigit() and i + 1 < len(toks) and toks[i + 1].lower().strip(".,") in MOIS:
skip = True; continue
if skip and t.lower().strip(".,") in MOIS:
skip = False; continue
reste.append(t)
texte = " ".join(reste).strip()
cat = _match_cat(texte, cats)
libelle = texte
if cat:
libelle = re.sub(re.escape(cat.nom), "", texte, flags=re.I).strip(" -·,")
lignes.append(_row(dt, libelle or texte or "(sans libellé)", montant, taux,
RegimeTva.NATIONALE, cat, n))
erreurs = [] if lignes else [
"Aucune ligne exploitable détectée. Réessaie avec une capture plus nette, "
"ou utilise le CSV."]
return lignes, erreurs
# ---- Création -----------------------------------------------------------
def _creer(entreprise, dt, libelle, montant, taux, regime, categorie):
neutre = bool(categorie and categorie.exclure_calculs)
if montant > 0: if montant > 0:
if l["neutre"]: if neutre:
Paiement.objects.create(entreprise=entreprise, date=l["date"], Paiement.objects.create(entreprise=entreprise, date=dt, libelle=libelle,
libelle=l["libelle"], montant_total=montant, montant_total=montant, sens="credit", categorie=categorie)
sens="credit", categorie=l["categorie"]) return "neutre"
return "vente_neutre" client, _ = Client.objects.get_or_create(entreprise=entreprise, nom=libelle)
client, _ = Client.objects.get_or_create(entreprise=entreprise, nom=l["libelle"]) ht = (montant / (Decimal("1") + taux / Decimal("100"))).quantize(CENT)
ttc = montant annee = dt.year
ht = (ttc / (Decimal("1") + l["taux"] / Decimal("100"))).quantize(CENT)
annee = l["date"].year
base = Facture.objects.filter(entreprise=entreprise, base = Facture.objects.filter(entreprise=entreprise,
numero__startswith=f"{annee}-").count() + 1 numero__startswith=f"{annee}-").count() + 1
numero = f"{annee}-{base:03d}" numero = f"{annee}-{base:03d}"
while Facture.objects.filter(entreprise=entreprise, numero=numero).exists(): while Facture.objects.filter(entreprise=entreprise, numero=numero).exists():
base += 1 base += 1; numero = f"{annee}-{base:03d}"
numero = f"{annee}-{base:03d}" f = Facture.objects.create(entreprise=entreprise, client=client, numero=numero,
f = Facture.objects.create( activite=entreprise.activite, date_emission=dt,
entreprise=entreprise, client=client, numero=numero, date_encaissement=dt, statut="encaissee")
activite=l["activite"] or entreprise.activite, date_emission=l["date"], LigneFacture.objects.create(facture=f, designation=libelle, quantite=Decimal("1"),
date_encaissement=l["date"], statut="encaissee") prix_unitaire_ht=ht, taux_tva=taux)
LigneFacture.objects.create(facture=f, designation=l["libelle"],
quantite=Decimal("1"), prix_unitaire_ht=ht,
taux_tva=l["taux"])
f.recompute_totaux() f.recompute_totaux()
return "vente" return "vente"
# Dépense
abs_m = -montant abs_m = -montant
if l["regime"] in (RegimeTva.INTRACOM, RegimeTva.IMPORT): if regime in (RegimeTva.INTRACOM, RegimeTva.IMPORT) or neutre:
ttc, tva = abs_m, Decimal("0")
elif l["neutre"]:
ttc, tva = abs_m, Decimal("0") ttc, tva = abs_m, Decimal("0")
else: else:
ttc = abs_m ttc = abs_m
tva = (abs_m * l["taux"] / (Decimal("100") + l["taux"])).quantize(CENT) if l["taux"] else Decimal("0") tva = (abs_m * taux / (Decimal("100") + taux)).quantize(CENT) if taux else Decimal("0")
Depense.objects.create( Depense.objects.create(entreprise=entreprise, date=dt, libelle=libelle,
entreprise=entreprise, date=l["date"], libelle=l["libelle"], categorie=categorie, regime_tva=regime,
categorie=l["categorie"], activite=l["activite"], regime_tva=l["regime"], taux_tva=taux if regime != RegimeTva.NATIONALE else Decimal("0"),
taux_tva=l["taux"] if l["regime"] != RegimeTva.NATIONALE else Decimal("0"), montant_ttc=ttc, montant_tva=tva)
montant_ttc=ttc, montant_tva=tva)
return "depense" return "depense"
@@ -140,29 +241,57 @@ def _creer(entreprise, l):
def import_csv(request): def import_csv(request):
entreprises = Entreprise.accessibles(request.user) entreprises = Entreprise.accessibles(request.user)
cats = {c.nom.strip().lower(): c for c in Categorie.objects.all()} cats = {c.nom.strip().lower(): c for c in Categorie.objects.all()}
ctx = {"entreprises": entreprises, "colonnes": COLONNES} ctx = {"entreprises": entreprises,
"categories": Categorie.objects.filter(actif=True),
"activites": Activite.choices, "regimes": RegimeTva.choices,
"annee_defaut": date_cls.today().year}
if request.method == "POST": if request.method != "POST":
entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first() return render(request, "compta/import_csv.html", ctx)
action = request.POST.get("action")
ctx["entreprise_id"] = int(request.POST.get("entreprise") or 0)
entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first()
if action == "save":
if not entreprise:
ctx["erreurs"] = ["Sélectionne une entreprise."]
return render(request, "compta/import_csv.html", ctx)
dates = request.POST.getlist("l_date")
libelles = request.POST.getlist("l_libelle")
cat_ids = request.POST.getlist("l_categorie")
taux = request.POST.getlist("l_taux")
regimes = request.POST.getlist("l_regime")
montants = request.POST.getlist("l_montant")
cats_by_id = {str(c.pk): c for c in Categorie.objects.all()}
compteur = {}
for i in range(len(libelles)):
dt = _date(dates[i]) if i < len(dates) else None
montant = _dec(montants[i] if i < len(montants) else "0")
if dt is None or montant == 0:
continue
k = _creer(entreprise, dt, (libelles[i] or "").strip() or "(sans libellé)",
montant, _dec(taux[i] if i < len(taux) else "0"),
_regime(regimes[i] if i < len(regimes) else ""),
cats_by_id.get(cat_ids[i] if i < len(cat_ids) else ""))
compteur[k] = compteur.get(k, 0) + 1
ctx["resultat"] = compteur
return render(request, "compta/import_csv.html", ctx)
# Aperçu : CSV ou OCR
image = request.FILES.get("image")
if image:
annee = int(request.POST.get("annee") or date_cls.today().year)
lignes, erreurs = _parse_ocr(image, annee, cats)
ctx["source"] = "ocr"
else:
texte = request.POST.get("texte", "") texte = request.POST.get("texte", "")
fichier = request.FILES.get("fichier") fichier = request.FILES.get("fichier")
if fichier: if fichier:
texte = fichier.read().decode("utf-8-sig", "ignore") texte = fichier.read().decode("utf-8-sig", "ignore")
lignes, erreurs = _parse(texte, cats) lignes, erreurs = _parse_csv(texte, cats)
ctx.update({"lignes": lignes, "erreurs": erreurs, "texte": texte, ctx["texte"] = texte
"entreprise_id": int(request.POST.get("entreprise") or 0)}) ctx["source"] = "csv"
ctx["lignes"] = lignes
if request.POST.get("action") == "import": ctx["erreurs"] = erreurs
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) return render(request, "compta/import_csv.html", ctx)
+2
View File
@@ -3,3 +3,5 @@ psycopg2-binary>=2.9
gunicorn>=22.0 gunicorn>=22.0
python-dotenv>=1.0 python-dotenv>=1.0
whitenoise>=6.6 whitenoise>=6.6
Pillow>=10.0
pytesseract>=0.3.10