0a4ba2001b
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
315 lines
13 KiB
Python
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
|