Groupe Gestionnaire + import CSV des operations
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,12 +1,13 @@
|
||||
"""
|
||||
Charge les DONNÉES DE RÉFÉRENCE (barèmes + catégories) — sans données de démo.
|
||||
Idempotent : sûr à relancer en production après chaque mise à jour.
|
||||
Données de RÉFÉRENCE (barèmes + catégories + groupe Gestionnaire) — sans démo.
|
||||
Idempotent : sûr à relancer en production. Appelé par l'entrypoint au démarrage.
|
||||
|
||||
Usage : python manage.py seed_reference
|
||||
"""
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
|
||||
from compta.models import Bareme, Categorie, Activite
|
||||
|
||||
@@ -19,7 +20,6 @@ BAREMES_2026 = [
|
||||
Decimal("83600"), Decimal("37500"), Decimal("41250")),
|
||||
]
|
||||
|
||||
# exclure_calculs=True => mouvement neutre (hors calculs fiscaux).
|
||||
CATEGORIES = [
|
||||
("Vente de produits finis", "vente", False, 1),
|
||||
("Prestation de services", "vente", False, 2),
|
||||
@@ -35,6 +35,30 @@ CATEGORIES = [
|
||||
("Virement interne", "neutre", True, 5),
|
||||
]
|
||||
|
||||
# Permissions du groupe "Gestionnaire entreprise"
|
||||
GROUPE_NOM = "Gestionnaire entreprise"
|
||||
PERMS_COMPLETES = ["client", "facture", "lignefacture", "depense", "paiement"] # add/change/delete/view
|
||||
PERMS_CHANGE = ["entreprise"] # view + change (pas add/delete, ni gestion des accès)
|
||||
PERMS_VUE = ["categorie", "bareme"] # view seulement
|
||||
|
||||
|
||||
def charger_groupes():
|
||||
g, _ = Group.objects.get_or_create(name=GROUPE_NOM)
|
||||
perms = []
|
||||
for model in PERMS_COMPLETES:
|
||||
for action in ("add", "change", "delete", "view"):
|
||||
perms += list(Permission.objects.filter(
|
||||
content_type__app_label="compta", codename=f"{action}_{model}"))
|
||||
for model in PERMS_CHANGE:
|
||||
for action in ("view", "change"):
|
||||
perms += list(Permission.objects.filter(
|
||||
content_type__app_label="compta", codename=f"{action}_{model}"))
|
||||
for model in PERMS_VUE:
|
||||
perms += list(Permission.objects.filter(
|
||||
content_type__app_label="compta", codename=f"view_{model}"))
|
||||
g.permissions.set(perms)
|
||||
return g
|
||||
|
||||
|
||||
def charger_reference(stdout=None, style=None):
|
||||
for act, urssaf, ab, vl, sca, tb, tm in BAREMES_2026:
|
||||
@@ -47,14 +71,16 @@ def charger_reference(stdout=None, style=None):
|
||||
c, _ = Categorie.objects.update_or_create(
|
||||
nom=nom, defaults=dict(usage=usage, exclure_calculs=excl, ordre=ordre))
|
||||
cat[nom] = c
|
||||
g = charger_groupes()
|
||||
if stdout and style:
|
||||
stdout.write(style.SUCCESS(
|
||||
f"Référence chargée : {len(BAREMES_2026)} barèmes, {len(cat)} catégories."))
|
||||
f"Référence : {len(BAREMES_2026)} barèmes, {len(cat)} catégories, "
|
||||
f"groupe « {g.name} » ({g.permissions.count()} permissions)."))
|
||||
return cat
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Charge les barèmes et catégories de référence (sans démo)."
|
||||
help = "Charge barèmes, catégories et le groupe Gestionnaire (sans démo)."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
charger_reference(self.stdout, self.style)
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<a class="{% if vue == 'urssaf' %}on{% endif %}" href="/declaration/urssaf/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Déclaration URSSAF</a>
|
||||
<a class="{% if vue == 'tva' %}on{% endif %}" href="/declaration/tva/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Déclaration TVA</a>
|
||||
<a class="saisie" href="/saisie/">+ Saisir</a>
|
||||
<a href="/import/">Importer</a>
|
||||
<a href="/admin/">Admin</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>JB Compta — Import CSV</title>
|
||||
<style>
|
||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
||||
--accent:#38bdf8; --ok:#34d399; --bad:#f87171; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
|
||||
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; }
|
||||
.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; }
|
||||
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; }
|
||||
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>
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="card">
|
||||
{% csrf_token %}
|
||||
<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>
|
||||
<input type="file" name="fichier" accept=".csv,.txt">
|
||||
<label>…ou colle directement les lignes ici</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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if lignes %}
|
||||
<div class="card">
|
||||
<h2>Aperçu — {{ lignes|length }} ligne(s)</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>
|
||||
<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>
|
||||
</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>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from . import views_saisie
|
||||
from . import views_import
|
||||
|
||||
urlpatterns = [
|
||||
path("dashboard/", views.dashboard, name="dashboard"),
|
||||
@@ -8,6 +9,7 @@ urlpatterns = [
|
||||
path("declaration/urssaf/", views.declaration_urssaf, name="declaration_urssaf"),
|
||||
path("declaration/tva/", views.declaration_tva, name="declaration_tva"),
|
||||
path("saisie/", views_saisie.saisie_paiement, name="saisie_paiement"),
|
||||
path("import/", views_import.import_csv, name="import_csv"),
|
||||
path("export/ventes/", views.export_ventes, name="export_ventes"),
|
||||
path("export/tva/", views.export_tva, name="export_tva"),
|
||||
path("export/urssaf/", views.export_urssaf, name="export_urssaf"),
|
||||
|
||||
@@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user