Files
admin 0a4ba2001b 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
2026-07-11 20:27:51 +02:00

315 lines
13 KiB
Python

"""
Modèles de données — JB Compta.
"""
from decimal import Decimal
from django.conf import settings
from django.db import models
from django.db.models import Sum, Q
class Activite(models.TextChoices):
VENTE = "vente", "Vente de marchandises (BIC)"
SERVICE_BIC = "service_bic", "Prestation de services (BIC)"
SERVICE_BNC = "service_bnc", "Prestation de services / libéral (BNC)"
class StatutFacture(models.TextChoices):
BROUILLON = "brouillon", "Brouillon"
ENVOYEE = "envoyee", "Envoyée"
ENCAISSEE = "encaissee", "Encaissée"
ANNULEE = "annulee", "Annulée"
class RegimeTva(models.TextChoices):
NATIONALE = "nationale", "Nationale (TVA française facturée)"
INTRACOM = "intracom", "Intracommunautaire (autoliquidation)"
IMPORT = "import", "Import hors UE (autoliquidation)"
TAUX_TVA_CHOICES = [(Decimal("0"), "0 %"), (Decimal("5.5"), "5,5 %"),
(Decimal("10"), "10 %"), (Decimal("20"), "20 %")]
class Categorie(models.Model):
"""Catégorie globale (mutualisée). exclure_calculs=True => mouvement neutre."""
USAGE = [("depense", "Dépense"), ("vente", "Vente"), ("neutre", "Mouvement neutre")]
nom = models.CharField(max_length=100, unique=True)
usage = models.CharField(max_length=10, choices=USAGE, default="depense")
exclure_calculs = models.BooleanField(
"Neutre (exclue des calculs)", default=False,
help_text="Cochez pour les mouvements non fiscaux : TVA payée, "
"cotisations URSSAF, remboursement de TVA, virements…")
compte_pcg = models.CharField(
"Compte PCG (Indy)", max_length=10, blank=True, db_index=True,
help_text="Numéro de compte du plan comptable, pour le mapping Indy.")
actif = models.BooleanField(default=True)
ordre = models.IntegerField(default=0)
class Meta:
verbose_name = "Catégorie"
verbose_name_plural = "Catégories"
ordering = ["usage", "ordre", "nom"]
def __str__(self):
return self.nom
class Bareme(models.Model):
"""Barème mutualisé (partagé entre toutes les entreprises)."""
activite = models.CharField(max_length=20, choices=Activite.choices)
annee = models.PositiveIntegerField()
taux_urssaf = models.DecimalField("Taux cotisations URSSAF (%)", max_digits=5, decimal_places=2)
abattement_fiscal = models.DecimalField("Abattement forfaitaire (%)", max_digits=5, decimal_places=2)
taux_vl = models.DecimalField("Taux versement libératoire (%)", max_digits=4, decimal_places=2)
seuil_ca = models.DecimalField("Plafond CA micro (€)", max_digits=12, decimal_places=2)
seuil_tva_base = models.DecimalField("Seuil franchise TVA (€)", max_digits=12, decimal_places=2)
seuil_tva_majore = models.DecimalField("Seuil franchise TVA majoré (€)", max_digits=12, decimal_places=2)
class Meta:
verbose_name = "Barème"
verbose_name_plural = "Barèmes"
unique_together = ("activite", "annee")
ordering = ["-annee", "activite"]
def __str__(self):
return f"{self.get_activite_display()}{self.annee}"
class Entreprise(models.Model):
VUE_TVA = [("simplifie", "Résumé simplifié"), ("ca3", "Champs CA3 officiels")]
nom = models.CharField(max_length=200)
siret = models.CharField(max_length=20, blank=True)
activite = models.CharField(
"Activité principale", max_length=20, choices=Activite.choices,
default=Activite.SERVICE_BNC, help_text="Activité par défaut des nouvelles factures.")
franchise_tva = models.BooleanField(
"En franchise de TVA", default=False, help_text="Décochez si vous facturez la TVA.")
taux_tva_defaut = models.DecimalField(
"Taux de TVA par défaut (%)", max_digits=4, decimal_places=2, default=Decimal("20.00"))
versement_liberatoire = models.BooleanField("Option versement libératoire", default=False)
num_tva_intracom = models.CharField("N° TVA intracommunautaire", max_length=20, blank=True)
vue_tva = models.CharField(
"Mode d'affichage de la déclaration TVA", max_length=10, choices=VUE_TVA, default="simplifie")
indy_token = models.TextField(
"Token Indy (API)", blank=True,
help_text="Token JWT capté dans le navigateur, pour la synchronisation. Expire ~1 h.")
utilisateurs = models.ManyToManyField(
settings.AUTH_USER_MODEL, blank=True, related_name="entreprises",
verbose_name="Utilisateurs autorisés",
help_text="Utilisateurs ayant accès directement à cette entreprise.")
groupes = models.ManyToManyField(
"auth.Group", blank=True, related_name="entreprises",
verbose_name="Groupes autorisés",
help_text="Tous les membres de ces groupes ont accès à cette entreprise.")
email = models.EmailField(blank=True)
adresse = models.TextField(blank=True)
cree_le = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Entreprise"
verbose_name_plural = "Entreprises"
ordering = ["nom"]
def __str__(self):
return self.nom
def bareme(self, annee):
return Bareme.objects.filter(activite=self.activite, annee=annee).first()
@classmethod
def accessibles(cls, user):
if not user or not user.is_authenticated:
return cls.objects.none()
if user.is_superuser:
return cls.objects.all()
return cls.objects.filter(
Q(utilisateurs=user) | Q(groupes__in=user.groups.all())).distinct()
class Client(models.Model):
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="clients")
nom = models.CharField(max_length=200)
email = models.EmailField(blank=True)
siret = models.CharField(max_length=20, blank=True)
adresse = models.TextField(blank=True)
cree_le = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Client"
verbose_name_plural = "Clients"
ordering = ["nom"]
def __str__(self):
return self.nom
class Facture(models.Model):
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="factures")
client = models.ForeignKey(Client, on_delete=models.PROTECT, related_name="factures")
numero = models.CharField(max_length=50)
activite = models.CharField(max_length=20, choices=Activite.choices)
date_emission = models.DateField()
date_encaissement = models.DateField(
null=True, blank=True, help_text="Le CA micro est compté à l'encaissement.")
statut = models.CharField(max_length=12, choices=StatutFacture.choices, default=StatutFacture.BROUILLON)
montant_ht = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
montant_tva = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
montant_ttc = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
notes = models.TextField(blank=True)
indy_id = models.CharField(max_length=40, blank=True, db_index=True)
cree_le = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Facture"
verbose_name_plural = "Factures"
unique_together = ("entreprise", "numero")
ordering = ["-date_emission", "numero"]
def __str__(self):
return f"{self.numero}{self.client}"
def recompute_totaux(self, save=True):
ht = Decimal("0")
tva = Decimal("0")
for ligne in self.lignes.all():
l_ht = ligne.montant_ht
ht += l_ht
tva += (l_ht * ligne.taux_tva / Decimal("100"))
self.montant_ht = ht.quantize(Decimal("0.01"))
self.montant_tva = tva.quantize(Decimal("0.01"))
self.montant_ttc = (self.montant_ht + self.montant_tva)
if save:
super().save(update_fields=["montant_ht", "montant_tva", "montant_ttc"])
class LigneFacture(models.Model):
facture = models.ForeignKey(Facture, on_delete=models.CASCADE, related_name="lignes")
designation = models.CharField(max_length=255)
quantite = models.DecimalField(max_digits=12, decimal_places=3, default=Decimal("1"))
prix_unitaire_ht = models.DecimalField(max_digits=12, decimal_places=2)
taux_tva = models.DecimalField(max_digits=4, decimal_places=2, default=Decimal("0"),
choices=TAUX_TVA_CHOICES)
class Meta:
verbose_name = "Ligne de facture"
verbose_name_plural = "Lignes de facture"
def __str__(self):
return self.designation
@property
def montant_ht(self):
return (self.quantite * self.prix_unitaire_ht).quantize(Decimal("0.01"))
class Paiement(models.Model):
"""Opération bancaire réelle, à éclater en une ou plusieurs dépenses."""
SENS = [("debit", "Débit (achat)"), ("credit", "Crédit (encaissement)")]
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="paiements")
date = models.DateField()
libelle = models.CharField("Libellé bancaire", max_length=255)
montant_total = models.DecimalField(max_digits=12, decimal_places=2)
sens = models.CharField(max_length=6, choices=SENS, default="debit")
categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL, null=True, blank=True,
related_name="paiements",
help_text="Surtout pour les mouvements neutres.")
indy_id = models.CharField(max_length=40, blank=True, db_index=True)
cree_le = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Opération bancaire"
verbose_name_plural = "Opérations bancaires"
ordering = ["-date"]
def __str__(self):
return f"{self.date}{self.libelle} ({self.montant_total} €)"
@property
def reparti(self):
total = self.depenses.aggregate(s=Sum("montant_ttc"))["s"] or Decimal("0")
return total.quantize(Decimal("0.01"))
@property
def reste(self):
return (self.montant_total - self.reparti).quantize(Decimal("0.01"))
@property
def equilibre(self):
return abs(self.reste) < Decimal("0.01")
class Depense(models.Model):
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="depenses")
paiement = models.ForeignKey(Paiement, on_delete=models.SET_NULL, null=True, blank=True,
related_name="depenses", verbose_name="Opération bancaire",
help_text="Opération bancaire dont cette dépense fait partie.")
date = models.DateField()
activite = models.CharField(
max_length=20, choices=Activite.choices, blank=True,
help_text="Activité concernée (facultatif, pour la ventilation).")
categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL, null=True, blank=True,
related_name="depenses")
libelle = models.CharField(max_length=255)
montant_ttc = models.DecimalField(
"Montant payé (€)", max_digits=12, decimal_places=2,
help_text="Pour un achat UE/import, c'est le montant HT (sans TVA facturée).")
montant_tva = models.DecimalField(
"TVA déductible (€)", max_digits=12, decimal_places=2, default=Decimal("0"),
help_text="Calculée automatiquement depuis le montant payé et le taux.")
regime_tva = models.CharField(
"Régime de TVA", max_length=12, choices=RegimeTva.choices, default=RegimeTva.NATIONALE)
taux_tva = models.DecimalField(
"Taux de TVA (%)", max_digits=4, decimal_places=2, default=Decimal("20"),
choices=TAUX_TVA_CHOICES,
help_text="Achat national : la TVA déductible est calculée. UE/import : taux auto-liquidé.")
indy_id = models.CharField(max_length=40, blank=True, db_index=True)
cree_le = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Dépense"
verbose_name_plural = "Dépenses"
ordering = ["-date"]
def __str__(self):
return f"{self.date}{self.libelle}"
def save(self, *args, **kwargs):
t = self.taux_tva or Decimal("0")
if self.autoliquidee:
self.montant_tva = Decimal("0.00")
elif t:
self.montant_tva = ((self.montant_ttc or Decimal("0")) * t
/ (Decimal("100") + t)).quantize(Decimal("0.01"))
else:
self.montant_tva = Decimal("0.00")
super().save(*args, **kwargs)
@property
def neutre(self):
return bool(self.categorie and self.categorie.exclure_calculs)
@property
def autoliquidee(self):
return self.regime_tva in (RegimeTva.INTRACOM, RegimeTva.IMPORT)
@property
def montant_ht(self):
if self.autoliquidee:
return self.montant_ttc.quantize(Decimal("0.01"))
return (self.montant_ttc - self.montant_tva).quantize(Decimal("0.01"))
@property
def tva_autoliquidee(self):
if self.autoliquidee and not self.neutre:
return (self.montant_ttc * self.taux_tva / Decimal("100")).quantize(Decimal("0.01"))
return Decimal("0.00")
@property
def tva_deductible_value(self):
if self.neutre:
return Decimal("0.00")
if self.autoliquidee:
return self.tva_autoliquidee
return self.montant_tva