OCR pytesseract dans l'import + apercu editable
This commit is contained in:
+6
-2
@@ -5,13 +5,17 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
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 .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
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
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>JB Compta — Import CSV</title>
|
||||
<title>JB Compta — Import</title>
|
||||
<style>
|
||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
||||
--accent:#38bdf8; --ok:#34d399; --bad:#f87171; }
|
||||
@@ -12,84 +12,103 @@
|
||||
background:var(--bg); color:var(--txt); padding:24px; }
|
||||
h1 { margin:0 0 4px; font-size:22px; } h2{font-size:15px;color:var(--muted);}
|
||||
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; }
|
||||
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;
|
||||
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);
|
||||
color:var(--txt); padding:9px 16px; font-size:14px; }
|
||||
button.primary { background:var(--accent); color:#04263a; border:none; font-weight:600; }
|
||||
.actions { display:flex; gap:12px; margin-top:12px; }
|
||||
table { width:100%; border-collapse:collapse; font-size:13px; }
|
||||
th,td { padding:6px 8px; border-bottom:1px solid #334155; text-align:right; }
|
||||
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; }
|
||||
th,td { padding:4px 6px; border-bottom:1px solid #334155; text-align:left; }
|
||||
td input, td select { padding:5px 7px; }
|
||||
.num { text-align:right; }
|
||||
code { background:#0f172a; padding:1px 6px; border-radius:5px; }
|
||||
.msg { padding:10px 14px; border-radius:8px; margin-bottom:8px; }
|
||||
.msg.bad { background:#7f1d1d; } .msg.ok { background:#064e3b; }
|
||||
.pill { padding:1px 8px; border-radius:99px; font-size:12px; background:#334155; }
|
||||
</style>
|
||||
</head>
|
||||
<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="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 resultat %}
|
||||
<div class="msg ok">Import terminé :
|
||||
{% 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
|
||||
<a href="/admin/compta/facture/">les factures</a>.
|
||||
</div>
|
||||
Voir <a href="/admin/compta/depense/">dépenses</a> · <a href="/admin/compta/facture/">factures</a>.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not lignes %}
|
||||
<form method="post" enctype="multipart/form-data" class="card">
|
||||
{% csrf_token %}
|
||||
<div class="grid3">
|
||||
<div>
|
||||
<label>Entreprise</label>
|
||||
<select name="entreprise" required>
|
||||
{% for e in entreprises %}<option value="{{ e.pk }}" {% if e.pk == entreprise_id %}selected{% endif %}>{{ e.nom }}</option>{% endfor %}
|
||||
</select>
|
||||
<label>Fichier CSV</label>
|
||||
</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">
|
||||
<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>
|
||||
<div class="actions">
|
||||
<button type="submit" name="action" value="preview">Aperçu</button>
|
||||
<button type="submit" name="action" value="import" class="primary">Importer</button>
|
||||
<button type="submit" name="action" value="preview" class="primary">Analyser / Aperçu</button>
|
||||
</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>
|
||||
{% endif %}
|
||||
|
||||
{% if lignes %}
|
||||
<div class="card">
|
||||
<h2>Aperçu — {{ lignes|length }} ligne(s)</h2>
|
||||
<form method="post" class="card">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="entreprise" value="{{ entreprise_id }}">
|
||||
<h2>Aperçu éditable — {{ lignes|length }} ligne(s) {% if source == 'ocr' %}(OCR){% endif %}</h2>
|
||||
<table>
|
||||
<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>
|
||||
<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>
|
||||
<tbody>
|
||||
{% for l in lignes %}
|
||||
<tr>
|
||||
<td>{{ l.n }}</td><td>{{ l.date }}</td><td>{{ l.libelle }}</td>
|
||||
<td>{{ l.categorie_nom }}{% if l.categorie_nom and not l.categorie %} ⚠️{% endif %}</td>
|
||||
<td><span class="pill">{{ l.type }}</span></td>
|
||||
<td>{{ l.taux }} %</td><td>{{ l.regime }}</td><td>{{ l.montant }} €</td>
|
||||
<td><input type="date" name="l_date" value="{{ l.date }}" style="width:140px"></td>
|
||||
<td><input type="text" name="l_libelle" value="{{ l.libelle }}"></td>
|
||||
<td>
|
||||
<select name="l_categorie">
|
||||
<option value="">—</option>
|
||||
{% for c in categories %}<option value="{{ c.pk }}" {% if c.pk == l.categorie_id %}selected{% endif %}>{{ c.nom }}</option>{% endfor %}
|
||||
</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="sub" style="margin-top:8px">⚠️ = catégorie non reconnue (sera laissée vide). Vérifie, puis clique « Importer ».</div>
|
||||
<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>
|
||||
</form>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+211
-82
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
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 d'opérations : par CSV (collé/uploadé) ou par OCR d'une capture d'écran
|
||||
(pytesseract). Dans les deux cas, un aperçu ÉDITABLE permet de corriger avant
|
||||
d'enregistrer (l'OCR n'est jamais parfait).
|
||||
"""
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
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.shortcuts import render
|
||||
@@ -15,13 +15,20 @@ 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")
|
||||
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"):
|
||||
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):
|
||||
return Decimal(d)
|
||||
|
||||
@@ -38,9 +45,8 @@ def _date(s):
|
||||
|
||||
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, "")
|
||||
return {"vente": "vente", "service_bic": "service_bic", "service_bnc": "service_bnc",
|
||||
"bic": "service_bic", "bnc": "service_bnc"}.get(s, "")
|
||||
|
||||
|
||||
def _regime(s):
|
||||
@@ -52,17 +58,36 @@ def _regime(s):
|
||||
return RegimeTva.NATIONALE
|
||||
|
||||
|
||||
def _parse(text, cats):
|
||||
"""Retourne (lignes, erreurs). Chaque ligne = dict prêt à créer + aperçu."""
|
||||
def _match_cat(text, cats):
|
||||
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("")
|
||||
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
|
||||
rows = list(csv.reader(io.StringIO(text), delimiter=delim))
|
||||
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):
|
||||
if not any(c.strip() for c in raw):
|
||||
continue
|
||||
@@ -70,68 +95,144 @@ def _parse(text, cats):
|
||||
dt = _date(c[0])
|
||||
montant = _dec(c[6])
|
||||
if dt is None:
|
||||
erreurs.append(f"Ligne {n} : date invalide « {c[0]} ».")
|
||||
continue
|
||||
erreurs.append(f"Ligne {n} : date invalide « {c[0]} »."); continue
|
||||
if montant == 0:
|
||||
erreurs.append(f"Ligne {n} : montant nul.")
|
||||
continue
|
||||
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,
|
||||
})
|
||||
lignes.append(_row(dt, (c[1] or "").strip() or "(sans libellé)", montant,
|
||||
_dec(c[4]), _regime(c[5]), cat, n))
|
||||
return lignes, erreurs
|
||||
|
||||
|
||||
def _creer(entreprise, l):
|
||||
montant = l["montant"]
|
||||
# ---- Parsing OCR --------------------------------------------------------
|
||||
|
||||
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 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
|
||||
if neutre:
|
||||
Paiement.objects.create(entreprise=entreprise, date=dt, libelle=libelle,
|
||||
montant_total=montant, sens="credit", categorie=categorie)
|
||||
return "neutre"
|
||||
client, _ = Client.objects.get_or_create(entreprise=entreprise, nom=libelle)
|
||||
ht = (montant / (Decimal("1") + taux / Decimal("100"))).quantize(CENT)
|
||||
annee = dt.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"])
|
||||
base += 1; numero = f"{annee}-{base:03d}"
|
||||
f = Facture.objects.create(entreprise=entreprise, client=client, numero=numero,
|
||||
activite=entreprise.activite, date_emission=dt,
|
||||
date_encaissement=dt, statut="encaissee")
|
||||
LigneFacture.objects.create(facture=f, designation=libelle, quantite=Decimal("1"),
|
||||
prix_unitaire_ht=ht, taux_tva=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"]:
|
||||
if regime in (RegimeTva.INTRACOM, RegimeTva.IMPORT) or 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"),
|
||||
tva = (abs_m * taux / (Decimal("100") + taux)).quantize(CENT) if taux else Decimal("0")
|
||||
Depense.objects.create(entreprise=entreprise, date=dt, libelle=libelle,
|
||||
categorie=categorie, regime_tva=regime,
|
||||
taux_tva=taux if regime != RegimeTva.NATIONALE else Decimal("0"),
|
||||
montant_ttc=ttc, montant_tva=tva)
|
||||
return "depense"
|
||||
|
||||
@@ -140,29 +241,57 @@ def _creer(entreprise, l):
|
||||
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}
|
||||
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":
|
||||
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", "")
|
||||
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
|
||||
lignes, erreurs = _parse_csv(texte, cats)
|
||||
ctx["texte"] = texte
|
||||
ctx["source"] = "csv"
|
||||
ctx["lignes"] = lignes
|
||||
ctx["erreurs"] = erreurs
|
||||
return render(request, "compta/import_csv.html", ctx)
|
||||
|
||||
@@ -3,3 +3,5 @@ psycopg2-binary>=2.9
|
||||
gunicorn>=22.0
|
||||
python-dotenv>=1.0
|
||||
whitenoise>=6.6
|
||||
Pillow>=10.0
|
||||
pytesseract>=0.3.10
|
||||
|
||||
Reference in New Issue
Block a user