JB Compta v1.1 - multi-activité

This commit is contained in:
2026-06-29 23:56:40 +02:00
commit 1716cebfbd
34 changed files with 1568 additions and 0 deletions
View File
+65
View File
@@ -0,0 +1,65 @@
from django.contrib import admin
from django.utils import timezone
from .models import (
Bareme, Entreprise, Client, Facture, LigneFacture, Depense,
)
@admin.register(Bareme)
class BaremeAdmin(admin.ModelAdmin):
list_display = ("annee", "activite", "taux_urssaf", "abattement_fiscal",
"taux_vl", "seuil_ca", "seuil_tva_base")
list_filter = ("annee", "activite")
@admin.register(Entreprise)
class EntrepriseAdmin(admin.ModelAdmin):
list_display = ("nom", "activite", "franchise_tva",
"versement_liberatoire", "email")
list_filter = ("activite", "franchise_tva", "versement_liberatoire")
search_fields = ("nom", "siret")
@admin.register(Client)
class ClientAdmin(admin.ModelAdmin):
list_display = ("nom", "entreprise", "email", "siret")
list_filter = ("entreprise",)
search_fields = ("nom", "email", "siret")
class LigneFactureInline(admin.TabularInline):
model = LigneFacture
extra = 1
@admin.action(description="Marquer comme encaissée (aujourd'hui)")
def marquer_encaissee(modeladmin, request, queryset):
queryset.update(statut="encaissee", date_encaissement=timezone.now().date())
@admin.register(Facture)
class FactureAdmin(admin.ModelAdmin):
list_display = ("numero", "entreprise", "client", "date_emission",
"date_encaissement", "statut", "montant_ht",
"montant_tva", "montant_ttc")
list_filter = ("entreprise", "statut", "activite", "date_emission")
search_fields = ("numero", "client__nom")
date_hierarchy = "date_emission"
inlines = [LigneFactureInline]
actions = [marquer_encaissee]
readonly_fields = ("montant_ht", "montant_tva", "montant_ttc")
def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
# Recalcule les totaux à partir des lignes après sauvegarde
form.instance.recompute_totaux()
@admin.register(Depense)
class DepenseAdmin(admin.ModelAdmin):
list_display = ("date", "entreprise", "libelle", "activite", "categorie",
"montant_ttc", "montant_tva")
list_filter = ("entreprise", "activite", "categorie", "date")
search_fields = ("libelle", "categorie")
date_hierarchy = "date"
+7
View File
@@ -0,0 +1,7 @@
from django.apps import AppConfig
class ComptaConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "compta"
verbose_name = "Compta micro-entreprise"
View File
+93
View File
@@ -0,0 +1,93 @@
"""
Charge les barèmes 2026 et un jeu de données d'exemple (multi-activité).
Usage : python manage.py seed_demo
"""
from decimal import Decimal
from datetime import date
from django.core.management.base import BaseCommand
from compta.models import (
Bareme, Entreprise, Client, Facture, LigneFacture, Depense, Activite,
StatutFacture,
)
# Barèmes officiels 2026 (voir docs/regles-fiscales-2026.md)
BAREMES_2026 = [
# activite, taux_urssaf, abattement, taux_vl, seuil_ca, tva_base, tva_majore
(Activite.VENTE, Decimal("12.30"), Decimal("71"), Decimal("1.0"),
Decimal("203100"), Decimal("85000"), Decimal("93500")),
(Activite.SERVICE_BIC, Decimal("21.20"), Decimal("50"), Decimal("1.7"),
Decimal("83600"), Decimal("37500"), Decimal("41250")),
(Activite.SERVICE_BNC, Decimal("25.60"), Decimal("34"), Decimal("2.2"),
Decimal("83600"), Decimal("37500"), Decimal("41250")),
]
class Command(BaseCommand):
help = "Charge les barèmes 2026 et des données d'exemple."
def handle(self, *args, **options):
for act, urssaf, ab, vl, sca, tb, tm in BAREMES_2026:
Bareme.objects.update_or_create(
activite=act, annee=2026,
defaults=dict(taux_urssaf=urssaf, abattement_fiscal=ab,
taux_vl=vl, seuil_ca=sca,
seuil_tva_base=tb, seuil_tva_majore=tm),
)
self.stdout.write(self.style.SUCCESS("Barèmes 2026 chargés."))
# Entreprise multi-activité : impression 3D (Vente BIC) + aide info (Presta BIC)
ent, _ = Entreprise.objects.get_or_create(
nom="JB Conseil",
defaults=dict(activite=Activite.SERVICE_BIC, franchise_tva=False,
taux_tva_defaut=Decimal("20.00"),
versement_liberatoire=True, email="jb@example.fr"),
)
clients = {}
for nom, mail in [("Acme SARL", "contact@acme.fr"),
("Studio Lumiere", "hello@studiolumiere.fr"),
("Mairie de Ville", "compta@ville.fr")]:
c, _ = Client.objects.get_or_create(entreprise=ent, nom=nom,
defaults=dict(email=mail))
clients[nom] = c
V, S = Activite.VENTE, Activite.SERVICE_BIC
factures = [
# numero, client, activite, emission, encaissement, statut, lignes
("2026-001", "Acme SARL", S, date(2026, 1, 10), date(2026, 1, 25),
StatutFacture.ENCAISSEE, [("Dépannage informatique", 2, 600)]),
("2026-002", "Studio Lumiere", V, date(2026, 1, 18), date(2026, 2, 5),
StatutFacture.ENCAISSEE, [("Impression 3D - lot pièces", 1, 800)]),
("2026-003", "Mairie de Ville", S, date(2026, 2, 2), date(2026, 2, 20),
StatutFacture.ENCAISSEE, [("Maintenance parc", 1, 2500)]),
("2026-004", "Acme SARL", V, date(2026, 3, 1), date(2026, 3, 15),
StatutFacture.ENCAISSEE, [("Impression 3D - prototype", 1, 1500)]),
("2026-005", "Studio Lumiere", S, date(2026, 3, 20), None,
StatutFacture.ENVOYEE, [("Audit poste de travail", 1, 950)]),
]
for numero, client_nom, activite, emis, enc, statut, lignes in factures:
f, created = Facture.objects.get_or_create(
entreprise=ent, numero=numero,
defaults=dict(client=clients[client_nom], activite=activite,
date_emission=emis, date_encaissement=enc,
statut=statut),
)
if created:
for des, qte, pu in lignes:
LigneFacture.objects.create(
facture=f, designation=des, quantite=Decimal(qte),
prix_unitaire_ht=Decimal(pu), taux_tva=Decimal("20"))
f.recompute_totaux()
Depense.objects.get_or_create(
entreprise=ent, libelle="Bobines PLA/PETG", date=date(2026, 1, 15),
defaults=dict(categorie="Matiere", activite=Activite.VENTE,
montant_ttc=Decimal("96.00"), montant_tva=Decimal("16.00")))
Depense.objects.get_or_create(
entreprise=ent, libelle="Abonnement outils", date=date(2026, 2, 10),
defaults=dict(categorie="Logiciel", activite=Activite.SERVICE_BIC,
montant_ttc=Decimal("34.80"), montant_tva=Decimal("5.80")))
self.stdout.write(self.style.SUCCESS("Données d'exemple chargées."))
+130
View File
@@ -0,0 +1,130 @@
# Generated by Django 5.2.15 on 2026-06-29 20:53
import django.db.models.deletion
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Entreprise',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nom', models.CharField(max_length=200)),
('siret', models.CharField(blank=True, max_length=20)),
('activite', models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], default='service_bnc', max_length=20)),
('franchise_tva', models.BooleanField(default=False, help_text='Décochez si vous facturez et déclarez la TVA.', verbose_name='En franchise de TVA')),
('taux_tva_defaut', models.DecimalField(decimal_places=2, default=Decimal('20.00'), max_digits=4, verbose_name='Taux de TVA par défaut (%)')),
('versement_liberatoire', models.BooleanField(default=False, verbose_name='Option versement libératoire')),
('email', models.EmailField(blank=True, max_length=254)),
('adresse', models.TextField(blank=True)),
('cree_le', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Entreprise',
'verbose_name_plural': 'Entreprises',
'ordering': ['nom'],
},
),
migrations.CreateModel(
name='Bareme',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('activite', models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], max_length=20)),
('annee', models.PositiveIntegerField()),
('taux_urssaf', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Taux cotisations URSSAF (%)')),
('abattement_fiscal', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Abattement forfaitaire (%)')),
('taux_vl', models.DecimalField(decimal_places=2, max_digits=4, verbose_name='Taux versement libératoire (%)')),
('seuil_ca', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Plafond CA micro (€)')),
('seuil_tva_base', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Seuil franchise TVA (€)')),
('seuil_tva_majore', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Seuil franchise TVA majoré (€)')),
],
options={
'verbose_name': 'Barème',
'verbose_name_plural': 'Barèmes',
'ordering': ['-annee', 'activite'],
'unique_together': {('activite', 'annee')},
},
),
migrations.CreateModel(
name='Depense',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('categorie', models.CharField(blank=True, max_length=100)),
('libelle', models.CharField(max_length=255)),
('montant_ttc', models.DecimalField(decimal_places=2, max_digits=12)),
('montant_tva', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12, verbose_name='TVA déductible (€)')),
('cree_le', models.DateTimeField(auto_now_add=True)),
('entreprise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='depenses', to='compta.entreprise')),
],
options={
'verbose_name': 'Dépense',
'verbose_name_plural': 'Dépenses',
'ordering': ['-date'],
},
),
migrations.CreateModel(
name='Client',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nom', models.CharField(max_length=200)),
('email', models.EmailField(blank=True, max_length=254)),
('siret', models.CharField(blank=True, max_length=20)),
('adresse', models.TextField(blank=True)),
('cree_le', models.DateTimeField(auto_now_add=True)),
('entreprise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clients', to='compta.entreprise')),
],
options={
'verbose_name': 'Client',
'verbose_name_plural': 'Clients',
'ordering': ['nom'],
},
),
migrations.CreateModel(
name='Facture',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('numero', models.CharField(max_length=50)),
('activite', models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], max_length=20)),
('date_emission', models.DateField()),
('date_encaissement', models.DateField(blank=True, help_text="À renseigner une fois la facture payée. Le CA micro est compté à l'encaissement.", null=True)),
('statut', models.CharField(choices=[('brouillon', 'Brouillon'), ('envoyee', 'Envoyée'), ('encaissee', 'Encaissée'), ('annulee', 'Annulée')], default='brouillon', max_length=12)),
('montant_ht', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12)),
('montant_tva', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12)),
('montant_ttc', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12)),
('notes', models.TextField(blank=True)),
('cree_le', models.DateTimeField(auto_now_add=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='factures', to='compta.client')),
('entreprise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='factures', to='compta.entreprise')),
],
options={
'verbose_name': 'Facture',
'verbose_name_plural': 'Factures',
'ordering': ['-date_emission', 'numero'],
'unique_together': {('entreprise', 'numero')},
},
),
migrations.CreateModel(
name='LigneFacture',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('designation', models.CharField(max_length=255)),
('quantite', models.DecimalField(decimal_places=3, default=Decimal('1'), max_digits=12)),
('prix_unitaire_ht', models.DecimalField(decimal_places=2, max_digits=12)),
('taux_tva', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=4)),
('facture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lignes', to='compta.facture')),
],
options={
'verbose_name': 'Ligne de facture',
'verbose_name_plural': 'Lignes de facture',
},
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.2.15 on 2026-06-29 21:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compta', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='depense',
name='activite',
field=models.CharField(blank=True, choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], help_text='Activité concernée (facultatif, pour la ventilation).', max_length=20),
),
migrations.AlterField(
model_name='entreprise',
name='activite',
field=models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], default='service_bnc', help_text='Activité par défaut des nouvelles factures. Une entreprise peut avoir plusieurs activités : chaque facture/dépense porte la sienne.', max_length=20, verbose_name='Activité principale'),
),
]
View File
+199
View File
@@ -0,0 +1,199 @@
"""
Modèles de données — JB Compta.
Conçus pour rester valables si le statut évolue : les taux et seuils
ne sont jamais codés en dur, ils vivent dans le modèle `Bareme`
(par activité et par année).
"""
from decimal import Decimal
from django.db import models
from django.db.models import Sum
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 Bareme(models.Model):
"""Taux et seuils officiels, par activité et par année (paramétrable)."""
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):
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. "
"Une entreprise peut avoir plusieurs activités : "
"chaque facture/dépense porte la sienne.")
franchise_tva = models.BooleanField(
"En franchise de TVA", default=False,
help_text="Décochez si vous facturez et déclarez 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)
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()
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="À renseigner une fois la facture payée. "
"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):
"""Recalcule HT/TVA/TTC à partir des lignes."""
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"))
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 Depense(models.Model):
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
related_name="depenses")
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.CharField(max_length=100, blank=True)
libelle = models.CharField(max_length=255)
montant_ttc = models.DecimalField(max_digits=12, decimal_places=2)
montant_tva = models.DecimalField(
"TVA déductible (€)", max_digits=12, decimal_places=2,
default=Decimal("0"))
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}"
@property
def montant_ht(self):
return (self.montant_ttc - self.montant_tva).quantize(Decimal("0.01"))
+154
View File
@@ -0,0 +1,154 @@
"""
Logique fiscale — JB Compta.
Bases de calcul : CA HORS TAXES (HT) encaissé.
- Base des cotisations URSSAF et du versement libératoire (assujetti TVA).
- En franchise de TVA, HT = TTC : le calcul reste correct.
Multi-activité : une entreprise peut exercer plusieurs activités (ex. Vente BIC
+ Prestation BIC). Les taux URSSAF, abattements, taux de versement libératoire et
seuils diffèrent par activité, donc tout est ventilé par activité puis totalisé.
TVA à reverser = TVA collectée (ventes encaissées) TVA déductible (dépenses).
"""
from decimal import Decimal
from django.db.models import Sum
from django.db.models.functions import TruncMonth
from .models import Facture, Depense, Bareme, StatutFacture, Activite
CENT = Decimal("0.01")
def _q(value):
return (value or Decimal("0")).quantize(CENT)
def factures_encaissees(entreprise, annee):
return Facture.objects.filter(
entreprise=entreprise,
statut=StatutFacture.ENCAISSEE,
date_encaissement__year=annee,
)
def ca_ht_par_activite(entreprise, annee):
"""Dict {activite: CA HT encaissé}."""
rows = (
factures_encaissees(entreprise, annee)
.values("activite")
.annotate(total=Sum("montant_ht"))
)
return {r["activite"]: _q(r["total"]) for r in rows}
def ca_ht(entreprise, annee):
agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_ht"))
return _q(agg["s"])
def tva_collectee(entreprise, annee):
agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_tva"))
return _q(agg["s"])
def tva_deductible(entreprise, annee):
agg = Depense.objects.filter(
entreprise=entreprise, date__year=annee
).aggregate(s=Sum("montant_tva"))
return _q(agg["s"])
def ca_ht_mensuel(entreprise, annee):
"""Liste de 12 valeurs (index 0 = janvier) du CA HT encaissé (toutes activités)."""
rows = (
factures_encaissees(entreprise, annee)
.annotate(mois=TruncMonth("date_encaissement"))
.values("mois")
.annotate(total=Sum("montant_ht"))
)
mensuel = [Decimal("0")] * 12
for r in rows:
if r["mois"]:
mensuel[r["mois"].month - 1] = _q(r["total"])
return mensuel
def detail_activite(activite, ca, bareme):
"""Calculs fiscaux pour une activité donnée."""
label = dict(Activite.choices).get(activite, activite)
d = {"activite": activite, "label": label, "ca_ht": ca, "bareme": bareme}
if bareme:
d.update({
"taux_urssaf": bareme.taux_urssaf,
"cotisations": _q(ca * bareme.taux_urssaf / Decimal("100")),
"taux_vl": bareme.taux_vl,
"versement_liberatoire": _q(ca * bareme.taux_vl / Decimal("100")),
"abattement_fiscal": bareme.abattement_fiscal,
"revenu_imposable": _q(
ca * (Decimal("100") - bareme.abattement_fiscal) / Decimal("100")),
"seuil_ca": bareme.seuil_ca,
"pct_seuil_ca": (_q(ca / bareme.seuil_ca * Decimal("100"))
if bareme.seuil_ca else Decimal("0")),
"seuil_tva_base": bareme.seuil_tva_base,
"depasse_seuil_tva": ca > bareme.seuil_tva_base,
"depasse_seuil_ca": ca > bareme.seuil_ca,
})
else:
d.update({
"taux_urssaf": None, "cotisations": None,
"taux_vl": None, "versement_liberatoire": None,
"abattement_fiscal": None, "revenu_imposable": None,
"seuil_ca": None, "pct_seuil_ca": None,
"seuil_tva_base": None, "depasse_seuil_tva": False,
"depasse_seuil_ca": False,
})
return d
def synthese(entreprise, annee):
"""Tableau de bord chiffré, ventilé par activité puis totalisé."""
ca_par_act = ca_ht_par_activite(entreprise, annee)
# On inclut toujours l'activité principale, même sans CA encore.
activites = set(ca_par_act) | {entreprise.activite}
baremes = {b.activite: b for b in Bareme.objects.filter(annee=annee)}
lignes = []
for act in sorted(activites):
ca = ca_par_act.get(act, Decimal("0"))
lignes.append(detail_activite(act, ca, baremes.get(act)))
# Totaux (on additionne ce qui est calculable)
def somme(cle):
vals = [l[cle] for l in lignes if l.get(cle) is not None]
return _q(sum(vals, Decimal("0"))) if vals else None
tva_col = tva_collectee(entreprise, annee)
tva_ded = tva_deductible(entreprise, annee)
return {
"entreprise": entreprise,
"annee": annee,
"lignes_activite": lignes,
"bareme_manquant": any(l["bareme"] is None and l["ca_ht"] for l in lignes),
"ca_ht": ca_ht(entreprise, annee),
"cotisations_urssaf": somme("cotisations"),
"versement_liberatoire": (
somme("versement_liberatoire") if entreprise.versement_liberatoire else None),
"revenu_imposable": somme("revenu_imposable"),
"tva_collectee": tva_col,
"tva_deductible": tva_ded,
"tva_a_reverser": _q(tva_col - tva_ded),
"ca_mensuel": ca_ht_mensuel(entreprise, annee),
}
def annees_disponibles(entreprise):
qs = (
Facture.objects.filter(entreprise=entreprise,
statut=StatutFacture.ENCAISSEE,
date_encaissement__isnull=False)
.dates("date_encaissement", "year")
)
return sorted({d.year for d in qs}, reverse=True)
+169
View File
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JB Compta — Tableau de bord</title>
<style>
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
* { box-sizing:border-box; }
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
background:var(--bg); color:var(--txt); padding:24px; }
h1 { margin:0 0 4px; font-size:22px; }
h2 { font-size:15px; color:var(--muted); margin:24px 0 10px; font-weight:600; }
a { color:var(--accent); }
.sub { color:var(--muted); margin-bottom:20px; }
form.filters { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:24px; }
select, button { background:var(--card); color:var(--txt); border:1px solid #334155;
border-radius:8px; padding:8px 12px; font-size:14px; }
button { cursor:pointer; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
gap:16px; margin-bottom:8px; }
.card { background:var(--card); border:1px solid #334155; border-radius:12px;
padding:16px; }
.card .label { color:var(--muted); font-size:13px; }
.card .value { font-size:26px; font-weight:600; margin-top:6px; }
.card .hint { color:var(--muted); font-size:12px; margin-top:4px; }
.ok{color:var(--ok)} .warn{color:var(--warn)} .bad{color:var(--bad)}
.bar { height:8px; background:#334155; border-radius:99px; overflow:hidden; margin-top:8px;}
.bar > span { display:block; height:100%; background:var(--accent); }
.row { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-top:8px; }
@media(max-width:800px){ .row{grid-template-columns:1fr;} }
.exports a { display:inline-block; margin-right:12px; }
canvas { max-width:100%; }
table { width:100%; border-collapse:collapse; font-size:14px; }
td,th { padding:8px; border-bottom:1px solid #334155; text-align:right; }
th:first-child, td:first-child { text-align:left; }
tr.total td { font-weight:600; border-top:2px solid #475569; }
</style>
</head>
<body>
{% if not entreprise %}
<h1>Aucune entreprise</h1>
<p class="sub">Crée une entreprise et des factures dans
<a href="/admin/">l'administration</a>, puis reviens ici.</p>
{% else %}
<h1>{{ entreprise.nom }}</h1>
<div class="sub">{{ entreprise.get_activite_display }} (principale) ·
{% if entreprise.franchise_tva %}Franchise de TVA{% else %}Assujetti TVA{% endif %}
{% if entreprise.versement_liberatoire %}· Versement libératoire{% endif %}
· Exercice {{ annee }}</div>
<form class="filters" method="get">
<select name="entreprise" onchange="this.form.submit()">
{% for e in entreprises %}
<option value="{{ e.pk }}" {% if e.pk == entreprise.pk %}selected{% endif %}>{{ e.nom }}</option>
{% endfor %}
</select>
<select name="annee" onchange="this.form.submit()">
{% for a in annees %}
<option value="{{ a }}" {% if a == annee %}selected{% endif %}>{{ a }}</option>
{% endfor %}
</select>
<a href="/admin/"><button type="button">Saisie / Admin</button></a>
</form>
{% if bareme_manquant %}
<div class="card bad" style="margin-bottom:16px;">
Une activité avec du CA n'a pas de barème {{ annee }}. Ajoute-le dans
<a href="/admin/compta/bareme/">Barèmes</a>.
</div>
{% endif %}
<!-- Totaux entreprise -->
<div class="grid">
<div class="card">
<div class="label">CA HT encaissé (total)</div>
<div class="value">{{ ca_ht }} €</div>
</div>
<div class="card">
<div class="label">Cotisations URSSAF (total)</div>
<div class="value">{{ cotisations_urssaf|default:"—" }} €</div>
</div>
<div class="card">
<div class="label">Impôt — versement libératoire</div>
<div class="value">{{ versement_liberatoire|default:"—" }} €</div>
</div>
<div class="card">
<div class="label">TVA à reverser</div>
<div class="value">{{ tva_a_reverser }} €</div>
<div class="hint">{{ tva_collectee }} collectée {{ tva_deductible }} déductible</div>
</div>
</div>
<!-- Ventilation par activité -->
<h2>Ventilation par activité</h2>
<div class="card">
<table>
<thead>
<tr>
<th>Activité</th><th>CA HT</th><th>% plafond</th>
<th>URSSAF</th><th>Impôt (VL)</th><th>Revenu imposable</th>
</tr>
</thead>
<tbody>
{% for l in lignes_activite %}
<tr>
<td>{{ l.label }}</td>
<td>{{ l.ca_ht }} €</td>
<td class="{% if l.depasse_seuil_ca %}bad{% endif %}">
{% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %}
</td>
<td>{% if l.cotisations is not None %}{{ l.cotisations }} €{% else %}—{% endif %}</td>
<td>{% if l.versement_liberatoire is not None %}{{ l.versement_liberatoire }} €{% else %}—{% endif %}</td>
<td>{% if l.revenu_imposable is not None %}{{ l.revenu_imposable }} €{% else %}—{% endif %}</td>
</tr>
{% endfor %}
<tr class="total">
<td>Total</td>
<td>{{ ca_ht }} €</td>
<td></td>
<td>{{ cotisations_urssaf|default:"—" }} €</td>
<td>{{ versement_liberatoire|default:"—" }} €</td>
<td>{{ revenu_imposable|default:"—" }} €</td>
</tr>
</tbody>
</table>
</div>
<div class="row">
<div class="card">
<div class="label" style="margin-bottom:8px;">CA HT mensuel (toutes activités)</div>
<canvas id="caChart" height="120"></canvas>
</div>
<div class="card">
<div class="label" style="margin-bottom:8px;">À provisionner (année)</div>
<table>
<tr><th>Poste</th><th>Montant</th></tr>
<tr><td>URSSAF</td><td>{{ cotisations_urssaf|default:"—" }} €</td></tr>
<tr><td>Impôt (VL)</td><td>{{ versement_liberatoire|default:"—" }} €</td></tr>
<tr><td>TVA</td><td>{{ tva_a_reverser }} €</td></tr>
</table>
<div class="exports" style="margin-top:16px;">
<div class="label" style="margin-bottom:6px;">Exports CSV</div>
<a href="{% url 'export_ventes' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">Ventes</a>
<a href="{% url 'export_tva' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">TVA</a>
<a href="{% url 'export_urssaf' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">URSSAF</a>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
const ctx = document.getElementById('caChart');
new Chart(ctx, {
type: 'bar',
data: {
labels: {{ mois_labels|safe }},
datasets: [{ label: 'CA HT (€)', data: {{ ca_mensuel_json|safe }},
backgroundColor: '#38bdf8' }]
},
options: { plugins:{legend:{display:false}},
scales:{ x:{ticks:{color:'#94a3b8'}}, y:{ticks:{color:'#94a3b8'}} } }
});
</script>
{% endif %}
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
from django.urls import path
from . import views
urlpatterns = [
path("dashboard/", views.dashboard, name="dashboard"),
path("export/ventes/", views.export_ventes, name="export_ventes"),
path("export/tva/", views.export_tva, name="export_tva"),
path("export/urssaf/", views.export_urssaf, name="export_urssaf"),
]
+89
View File
@@ -0,0 +1,89 @@
import csv
from datetime import date
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404
from .models import Entreprise
from . import services
def _entreprise_courante(request):
eid = request.GET.get("entreprise")
qs = Entreprise.objects.all()
if eid:
return get_object_or_404(Entreprise, pk=eid)
return qs.first()
@login_required
def dashboard(request):
entreprise = _entreprise_courante(request)
if entreprise is None:
return render(request, "compta/dashboard.html", {"entreprise": None})
annee = int(request.GET.get("annee", date.today().year))
ctx = services.synthese(entreprise, annee)
ctx.update({
"entreprises": Entreprise.objects.all(),
"annees": services.annees_disponibles(entreprise) or [annee],
"mois_labels": ["Jan", "Fév", "Mar", "Avr", "Mai", "Juin",
"Juil", "Août", "Sep", "Oct", "Nov", "Déc"],
"ca_mensuel_json": [float(v) for v in ctx["ca_mensuel"]],
})
return render(request, "compta/dashboard.html", ctx)
def _csv_response(filename):
resp = HttpResponse(content_type="text/csv; charset=utf-8")
resp["Content-Disposition"] = f'attachment; filename="{filename}"'
resp.write("") # BOM pour Excel
return resp
@login_required
def export_ventes(request):
entreprise = _entreprise_courante(request)
annee = int(request.GET.get("annee", date.today().year))
resp = _csv_response(f"ventes_{annee}.csv")
w = csv.writer(resp, delimiter=";")
w.writerow(["Numero", "Client", "Activite", "Date emission",
"Date encaissement", "Statut", "HT", "TVA", "TTC"])
for f in services.factures_encaissees(entreprise, annee).select_related("client"):
w.writerow([f.numero, f.client.nom, f.get_activite_display(),
f.date_emission, f.date_encaissement, f.get_statut_display(),
f.montant_ht, f.montant_tva, f.montant_ttc])
return resp
@login_required
def export_tva(request):
entreprise = _entreprise_courante(request)
annee = int(request.GET.get("annee", date.today().year))
s = services.synthese(entreprise, annee)
resp = _csv_response(f"tva_{annee}.csv")
w = csv.writer(resp, delimiter=";")
w.writerow(["Annee", "TVA collectee", "TVA deductible", "TVA a reverser"])
w.writerow([annee, s["tva_collectee"], s["tva_deductible"],
s["tva_a_reverser"]])
return resp
@login_required
def export_urssaf(request):
entreprise = _entreprise_courante(request)
annee = int(request.GET.get("annee", date.today().year))
s = services.synthese(entreprise, annee)
resp = _csv_response(f"urssaf_{annee}.csv")
w = csv.writer(resp, delimiter=";")
w.writerow(["Annee", "Activite", "CA HT", "Taux URSSAF", "Cotisations",
"Taux VL", "Versement liberatoire", "Abattement",
"Revenu imposable"])
for l in s["lignes_activite"]:
w.writerow([annee, l["label"], l["ca_ht"], l["taux_urssaf"],
l["cotisations"], l["taux_vl"], l["versement_liberatoire"],
l["abattement_fiscal"], l["revenu_imposable"]])
w.writerow([annee, "TOTAL", s["ca_ht"], "", s["cotisations_urssaf"],
"", s["versement_liberatoire"], "", s["revenu_imposable"]])
return resp