""" 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…") 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") 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) 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.") 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é.") 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): # TVA déductible calculée : incluse dans le TTC (national), nulle en autoliq. 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