""" Saisie unifiée : une opération bancaire signée. - Montant négatif -> dépense(s) (éclatement possible). - Montant positif -> facture (vente) + création de client à la volée, OU mouvement neutre (ex. remboursement de TVA). Cloisonnée : un utilisateur ne voit que ses entreprises. """ from decimal import Decimal, InvalidOperation from datetime import date from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect from .models import ( Entreprise, Client, Facture, LigneFacture, Paiement, Depense, Activite, RegimeTva, Categorie, ) def _dec(val, defaut="0"): try: return Decimal((val or defaut).replace(",", ".").strip() or defaut) except (InvalidOperation, AttributeError): return Decimal(defaut) def _numero_unique(entreprise, annee): base = Facture.objects.filter( entreprise=entreprise, numero__startswith=f"{annee}-").count() + 1 while True: numero = f"{annee}-{base:03d}" if not Facture.objects.filter(entreprise=entreprise, numero=numero).exists(): return numero base += 1 def _context(entreprises, post=None): return { "entreprises": entreprises, "clients": Client.objects.filter(entreprise__in=entreprises).select_related("entreprise"), "activites": Activite.choices, "regimes": RegimeTva.choices, "categories_depense": Categorie.objects.filter(actif=True).exclude(usage="vente"), "categories_vente": Categorie.objects.filter(actif=True, usage__in=["vente", "neutre"]), "today": date.today().isoformat(), "post": post, "vue": "saisie", } @login_required def saisie_paiement(request): entreprises = Entreprise.accessibles(request.user) if request.method != "POST": return render(request, "compta/saisie_paiement.html", _context(entreprises)) entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first() d = request.POST.get("date") or date.today().isoformat() libelle = (request.POST.get("libelle") or "").strip() montant_total = _dec(request.POST.get("montant_total")) annee = int(d[:4]) erreurs = [] if not entreprise: erreurs.append("Sélectionne une entreprise autorisée.") if not libelle: erreurs.append("Renseigne le libellé de l'opération.") if montant_total == 0: erreurs.append("Le montant ne peut pas être nul.") if erreurs: for e in erreurs: messages.error(request, e) return render(request, "compta/saisie_paiement.html", _context(entreprises, request.POST)) cats = {str(c.pk): c for c in Categorie.objects.all()} # --- CRÉDIT (positif) -> facture (vente) ou mouvement neutre --- if montant_total > 0: cat = cats.get(request.POST.get("v_categorie")) if cat and cat.exclure_calculs: Paiement.objects.create( entreprise=entreprise, date=d, libelle=libelle, montant_total=montant_total, sens="credit", categorie=cat) messages.success(request, f"Mouvement neutre « {libelle} » enregistré.") return redirect("saisie_paiement") client = None nouveau = (request.POST.get("v_client_nouveau") or "").strip() if nouveau: client, _ = Client.objects.get_or_create(entreprise=entreprise, nom=nouveau) elif request.POST.get("v_client"): client = Client.objects.filter( entreprise=entreprise, pk=request.POST.get("v_client")).first() if not client: messages.error(request, "Choisis un client existant ou saisis-en un nouveau.") return render(request, "compta/saisie_paiement.html", _context(entreprises, request.POST)) activite = request.POST.get("v_activite") or entreprise.activite taux = _dec(request.POST.get("v_taux"), "0") designation = (request.POST.get("v_designation") or libelle).strip() ttc = montant_total ht = (ttc / (Decimal("1") + taux / Decimal("100"))).quantize(Decimal("0.01")) f = Facture.objects.create( entreprise=entreprise, client=client, numero=_numero_unique(entreprise, annee), activite=activite, date_emission=d, date_encaissement=d, statut="encaissee") LigneFacture.objects.create(facture=f, designation=designation, quantite=Decimal("1"), prix_unitaire_ht=ht, taux_tva=taux) f.recompute_totaux() messages.success(request, f"Vente « {libelle} » enregistrée (facture {f.numero}).") return redirect("saisie_paiement") # --- DÉBIT (négatif) -> dépense(s), éclatement possible --- total_abs = -montant_total libelles = request.POST.getlist("l_libelle") categories = request.POST.getlist("l_categorie") activites = request.POST.getlist("l_activite") regimes = request.POST.getlist("l_regime") taux = request.POST.getlist("l_taux") montants = request.POST.getlist("l_montant") lignes = [] for i in range(len(libelles)): m = _dec(montants[i] if i < len(montants) else "0") lib = (libelles[i] or "").strip() if m == 0 and not lib: continue lignes.append({ "libelle": lib or "(sans libellé)", "categorie": cats.get(categories[i] if i < len(categories) else ""), "activite": activites[i] if i < len(activites) else "", "regime": regimes[i] if i < len(regimes) else RegimeTva.NATIONALE, "taux": _dec(taux[i] if i < len(taux) else "0"), "montant": m, }) somme = sum((l["montant"] for l in lignes), Decimal("0")) if not lignes: messages.error(request, "Ajoute au moins une ligne de dépense.") return render(request, "compta/saisie_paiement.html", _context(entreprises, request.POST)) if abs(somme - total_abs) >= Decimal("0.01"): messages.error(request, f"La somme des lignes ({somme} €) ne correspond pas au montant " f"({total_abs} €). Écart : {somme - total_abs} €.") return render(request, "compta/saisie_paiement.html", _context(entreprises, request.POST)) paiement = Paiement.objects.create( entreprise=entreprise, date=d, libelle=libelle, montant_total=total_abs, sens="debit") for l in lignes: # montant_tva est calculé automatiquement dans Depense.save() depuis le taux. Depense.objects.create( entreprise=entreprise, paiement=paiement, date=d, libelle=l["libelle"], categorie=l["categorie"], activite=l["activite"] or "", regime_tva=l["regime"], taux_tva=l["taux"], montant_ttc=l["montant"]) messages.success(request, f"Opération « {libelle} » enregistrée ({len(lignes)} dépense(s)).") return redirect("saisie_paiement")