Changes to be committed:
modified: .env.example new file: compta/__pycache__/__init__.cpython-310.pyc new file: compta/__pycache__/admin.cpython-310.pyc new file: compta/__pycache__/apps.cpython-310.pyc new file: compta/__pycache__/models.cpython-310.pyc modified: compta/admin.py new file: compta/categories_indy.py modified: compta/indy.py modified: compta/management/commands/seed_reference.py new file: compta/migrations/0010_categorie_compte_pcg.py modified: compta/models.py modified: compta/templates/compta/base.html new file: compta/templates/compta/gestion.html modified: compta/urls.py new file: compta/views_gestion.py new file: config/__pycache__/__init__.cpython-310.pyc new file: config/__pycache__/settings.cpython-310.pyc modified: config/settings.py modified: entrypoint.sh
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
Page de gestion des entrées / sorties : vue unifiée de TOUTES les opérations
|
||||
importées (recettes = factures, dépenses, opérations bancaires neutres).
|
||||
|
||||
- Filtrer (type, catégorie, régime, texte, dates), ordonner (date, montant, libellé).
|
||||
- Modifier via une popup (avec explications sur les catégories) : la TVA est
|
||||
recalculée automatiquement.
|
||||
- Supprimer.
|
||||
|
||||
Tout est cloisonné par entreprise (Entreprise.accessibles).
|
||||
"""
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from .models import (Entreprise, Facture, LigneFacture, Depense, Paiement,
|
||||
Categorie, Activite, RegimeTva, StatutFacture, TAUX_TVA_CHOICES)
|
||||
|
||||
CENT = Decimal("0.01")
|
||||
|
||||
# Explications spécifiques (affichées dans la popup). Sinon, texte générique par usage.
|
||||
EXPLIC = {
|
||||
"vente": "Recette imposable : entre dans le chiffre d'affaires (CA) et dans "
|
||||
"l'assiette des cotisations URSSAF. La TVA collectée est due.",
|
||||
"depense": "Charge professionnelle : ne réduit PAS le CA micro, mais la TVA "
|
||||
"payée est récupérable (TVA déductible) si vous êtes assujetti.",
|
||||
"neutre": "Mouvement neutre : n'entre ni dans le CA, ni dans les cotisations, "
|
||||
"ni dans la TVA. Pur mouvement de trésorerie (apport, prélèvement, "
|
||||
"impôt, virement interne, remboursement…).",
|
||||
}
|
||||
EXPLIC_SPE = {
|
||||
"Apport personnel": "Argent que vous injectez depuis vos fonds perso. Ce n'est "
|
||||
"pas une recette : aucun impact fiscal.",
|
||||
"Prélèvement personnel": "Argent que vous sortez pour vous. En micro, la "
|
||||
"rémunération n'est pas une charge : mouvement neutre.",
|
||||
"Remboursement reçu": "Remboursement d'un tiers (trop-perçu, avoir…). Ce n'est "
|
||||
"pas un revenu d'activité : neutre.",
|
||||
"Cotisation sociale Urssaf": "Vos cotisations URSSAF. Déjà prises en compte via "
|
||||
"le taux URSSAF : ne pas les recompter en charge.",
|
||||
"TVA payée": "Reversement de TVA à l'État. Neutre : c'est de la trésorerie, "
|
||||
"pas une charge.",
|
||||
"Remboursement de TVA": "Crédit de TVA remboursé par l'État. Neutre.",
|
||||
"Virement interne": "Transfert entre vos propres comptes. Aucun impact.",
|
||||
"CFE": "Cotisation Foncière des Entreprises. Impôt : suivi mais neutre côté TVA/CA.",
|
||||
}
|
||||
|
||||
|
||||
def _explication(cat):
|
||||
if cat is None:
|
||||
return ("Non catégorisé : pensez à choisir une catégorie pour un calcul "
|
||||
"fiscal correct.")
|
||||
return EXPLIC_SPE.get(cat.nom, EXPLIC.get(cat.usage, EXPLIC["depense"]))
|
||||
|
||||
|
||||
def _entreprise(request, pk=None):
|
||||
acc = Entreprise.accessibles(request.user)
|
||||
pk = pk or request.GET.get("entreprise") or request.POST.get("entreprise")
|
||||
if pk:
|
||||
return acc.filter(pk=pk).first()
|
||||
return acc.first()
|
||||
|
||||
|
||||
def _dec(v, d="0"):
|
||||
try:
|
||||
return Decimal(str(v).replace(",", ".").strip() or d)
|
||||
except (InvalidOperation, AttributeError):
|
||||
return Decimal(d)
|
||||
|
||||
|
||||
def _date(s):
|
||||
for fmt in ("%Y-%m-%d", "%d/%m/%Y"):
|
||||
try:
|
||||
return datetime.strptime((s or "").strip(), fmt).date()
|
||||
except ValueError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _rows(entreprise):
|
||||
"""Liste unifiée des opérations de l'entreprise (recettes, dépenses, neutres)."""
|
||||
rows = []
|
||||
for f in (Facture.objects.filter(entreprise=entreprise)
|
||||
.prefetch_related("lignes").select_related("client")):
|
||||
une_ligne = f.lignes.count() == 1
|
||||
rows.append({
|
||||
"type": "recette", "type_label": "Recette", "id": f.pk,
|
||||
"date": f.date_encaissement or f.date_emission,
|
||||
"libelle": str(f.client), "categorie": f"Facture {f.numero}",
|
||||
"categorie_id": "", "activite": f.activite,
|
||||
"regime": "nationale", "taux": f.lignes.first().taux_tva if une_ligne else "",
|
||||
"montant": f.montant_ttc, "tva": f.montant_tva, "neutre": False,
|
||||
"signe": Decimal("1"), "statut": f.statut,
|
||||
"date_encaissement": f.date_encaissement, "date_emission": f.date_emission,
|
||||
"editable_montant": une_ligne,
|
||||
})
|
||||
for d in Depense.objects.filter(entreprise=entreprise).select_related("categorie"):
|
||||
rows.append({
|
||||
"type": "depense", "type_label": "Dépense", "id": d.pk,
|
||||
"date": d.date, "libelle": d.libelle,
|
||||
"categorie": d.categorie.nom if d.categorie else "(à catégoriser)",
|
||||
"categorie_id": d.categorie_id or "", "activite": d.activite,
|
||||
"regime": d.regime_tva, "taux": d.taux_tva,
|
||||
"montant": -d.montant_ttc, "tva": d.tva_deductible_value,
|
||||
"neutre": d.neutre, "signe": Decimal("-1"), "editable_montant": True,
|
||||
})
|
||||
for p in Paiement.objects.filter(entreprise=entreprise).select_related("categorie"):
|
||||
rows.append({
|
||||
"type": "operation", "type_label": "Opération", "id": p.pk,
|
||||
"date": p.date, "libelle": p.libelle,
|
||||
"categorie": p.categorie.nom if p.categorie else "(non catégorisée)",
|
||||
"categorie_id": p.categorie_id or "", "activite": "",
|
||||
"regime": "", "taux": "",
|
||||
"montant": p.montant_total if p.sens == "credit" else -p.montant_total,
|
||||
"tva": Decimal("0"),
|
||||
"neutre": bool(p.categorie and p.categorie.exclure_calculs),
|
||||
"signe": Decimal("1") if p.sens == "credit" else Decimal("-1"),
|
||||
"sens": p.sens, "editable_montant": True,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def _filtrer_trier(rows, request):
|
||||
q = (request.GET.get("q") or "").strip().lower()
|
||||
ftype = request.GET.get("type") or ""
|
||||
fcat = request.GET.get("cat") or ""
|
||||
fregime = request.GET.get("regime") or ""
|
||||
dfrom = _date(request.GET.get("du"))
|
||||
dto = _date(request.GET.get("au"))
|
||||
tri = request.GET.get("tri") or "date"
|
||||
sens = request.GET.get("sens") or "desc"
|
||||
|
||||
def garde(r):
|
||||
if ftype and r["type"] != ftype:
|
||||
return False
|
||||
if fcat and str(r["categorie_id"]) != fcat:
|
||||
return False
|
||||
if fregime and r["regime"] != fregime:
|
||||
return False
|
||||
if q and q not in (r["libelle"] + " " + r["categorie"]).lower():
|
||||
return False
|
||||
if dfrom and r["date"] and r["date"] < dfrom:
|
||||
return False
|
||||
if dto and r["date"] and r["date"] > dto:
|
||||
return False
|
||||
return True
|
||||
|
||||
rows = [r for r in rows if garde(r)]
|
||||
cles = {"date": lambda r: (r["date"] or datetime(1900, 1, 1).date()),
|
||||
"montant": lambda r: r["montant"],
|
||||
"libelle": lambda r: r["libelle"].lower()}
|
||||
rows.sort(key=cles.get(tri, cles["date"]), reverse=(sens == "desc"))
|
||||
return rows, {"q": q, "type": ftype, "cat": fcat, "regime": fregime,
|
||||
"du": request.GET.get("du") or "", "au": request.GET.get("au") or "",
|
||||
"tri": tri, "sens": sens}
|
||||
|
||||
|
||||
@login_required
|
||||
def gestion(request):
|
||||
if request.method == "POST":
|
||||
return _traiter_post(request)
|
||||
|
||||
entreprise = _entreprise(request)
|
||||
if entreprise is None:
|
||||
return render(request, "compta/gestion.html", {"entreprise": None})
|
||||
|
||||
rows, filtres = _filtrer_trier(_rows(entreprise), request)
|
||||
total = sum((r["montant"] for r in rows), Decimal("0"))
|
||||
entrees = sum((r["montant"] for r in rows if r["montant"] > 0), Decimal("0"))
|
||||
sorties = sum((r["montant"] for r in rows if r["montant"] < 0), Decimal("0"))
|
||||
|
||||
cats = list(Categorie.objects.filter(actif=True))
|
||||
cats_json = {str(c.pk): {"nom": c.nom, "usage": c.usage,
|
||||
"neutre": c.exclure_calculs, "compte": c.compte_pcg,
|
||||
"explication": _explication(c)} for c in cats}
|
||||
|
||||
ctx = {
|
||||
"entreprise": entreprise,
|
||||
"entreprises": Entreprise.accessibles(request.user),
|
||||
"rows": rows, "filtres": filtres,
|
||||
"total": total, "entrees": entrees, "sorties": sorties, "nb": len(rows),
|
||||
"categories": cats, "cats_json": cats_json,
|
||||
"activites": Activite.choices, "regimes": RegimeTva.choices,
|
||||
"statuts": StatutFacture.choices, "taux_choices": TAUX_TVA_CHOICES,
|
||||
"vue": "gestion",
|
||||
}
|
||||
return render(request, "compta/gestion.html", ctx)
|
||||
|
||||
|
||||
def _retour(request, entreprise):
|
||||
base = reverse("gestion")
|
||||
qs = request.POST.get("retour_qs", "")
|
||||
url = f"{base}?entreprise={entreprise.pk}"
|
||||
if qs:
|
||||
url += "&" + qs
|
||||
return redirect(url)
|
||||
|
||||
|
||||
def _traiter_post(request):
|
||||
entreprise = _entreprise(request)
|
||||
if entreprise is None:
|
||||
return redirect(reverse("gestion"))
|
||||
action = request.POST.get("action")
|
||||
typ = request.POST.get("type")
|
||||
oid = request.POST.get("id")
|
||||
|
||||
modeles = {"recette": Facture, "depense": Depense, "operation": Paiement}
|
||||
Modele = modeles.get(typ)
|
||||
if not Modele:
|
||||
return _retour(request, entreprise)
|
||||
obj = Modele.objects.filter(entreprise=entreprise, pk=oid).first()
|
||||
if not obj:
|
||||
messages.error(request, "Opération introuvable.")
|
||||
return _retour(request, entreprise)
|
||||
|
||||
if action == "delete":
|
||||
obj.delete()
|
||||
messages.success(request, "Opération supprimée.")
|
||||
return _retour(request, entreprise)
|
||||
|
||||
# action == "edit"
|
||||
dt = _date(request.POST.get("date"))
|
||||
libelle = (request.POST.get("libelle") or "").strip()
|
||||
montant = _dec(request.POST.get("montant"))
|
||||
cat_id = request.POST.get("categorie") or ""
|
||||
cat = Categorie.objects.filter(pk=cat_id).first() if cat_id else None
|
||||
|
||||
if typ == "depense":
|
||||
if dt:
|
||||
obj.date = dt
|
||||
obj.libelle = libelle or obj.libelle
|
||||
obj.categorie = cat
|
||||
obj.activite = request.POST.get("activite") or ""
|
||||
obj.regime_tva = request.POST.get("regime") or RegimeTva.NATIONALE
|
||||
obj.taux_tva = _dec(request.POST.get("taux"), "0")
|
||||
obj.montant_ttc = abs(montant)
|
||||
obj.save() # recalcule la TVA
|
||||
elif typ == "operation":
|
||||
if dt:
|
||||
obj.date = dt
|
||||
obj.libelle = libelle or obj.libelle
|
||||
obj.categorie = cat
|
||||
obj.sens = request.POST.get("sens") or obj.sens
|
||||
obj.montant_total = abs(montant)
|
||||
obj.save()
|
||||
elif typ == "recette":
|
||||
de = _date(request.POST.get("date_emission")) or _date(request.POST.get("date"))
|
||||
if de:
|
||||
obj.date_emission = de
|
||||
obj.date_encaissement = _date(request.POST.get("date_encaissement"))
|
||||
obj.statut = request.POST.get("statut") or obj.statut
|
||||
ligne = obj.lignes.first()
|
||||
if ligne and obj.lignes.count() == 1:
|
||||
taux = _dec(request.POST.get("taux"), str(ligne.taux_tva))
|
||||
ttc = abs(montant) if montant else obj.montant_ttc
|
||||
ligne.prix_unitaire_ht = (ttc / (Decimal("1") + taux / Decimal("100"))).quantize(CENT)
|
||||
ligne.taux_tva = taux
|
||||
ligne.quantite = Decimal("1")
|
||||
ligne.save()
|
||||
obj.save()
|
||||
obj.recompute_totaux()
|
||||
|
||||
messages.success(request, "Opération modifiée.")
|
||||
return _retour(request, entreprise)
|
||||
Reference in New Issue
Block a user