Saisie unifiée + 4 vues dashboard (mensuel, déclarations URSSAF/TVA)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 5.2.15 on 2026-06-29 22:49
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('compta', '0005_categorie_entreprise_vue_tva_alter_depense_categorie'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='paiement',
|
||||||
|
name='categorie',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Surtout pour les mouvements neutres (remboursement TVA, virement…).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paiements', to='compta.categorie'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='depense',
|
||||||
|
name='paiement',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Opération bancaire dont cette dépense fait partie.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='depenses', to='compta.paiement'),
|
||||||
|
),
|
||||||
|
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.', max_length=20, verbose_name='Activité principale'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='facture',
|
||||||
|
name='date_encaissement',
|
||||||
|
field=models.DateField(blank=True, help_text="Le CA micro est compté à l'encaissement.", null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+39
-87
@@ -1,9 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Modèles de données — JB Compta.
|
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 decimal import Decimal
|
||||||
|
|
||||||
@@ -25,24 +21,14 @@ class StatutFacture(models.TextChoices):
|
|||||||
|
|
||||||
|
|
||||||
class RegimeTva(models.TextChoices):
|
class RegimeTva(models.TextChoices):
|
||||||
"""Régime de TVA d'une dépense (achat)."""
|
|
||||||
NATIONALE = "nationale", "Nationale (TVA française facturée)"
|
NATIONALE = "nationale", "Nationale (TVA française facturée)"
|
||||||
INTRACOM = "intracom", "Intracommunautaire (autoliquidation)"
|
INTRACOM = "intracom", "Intracommunautaire (autoliquidation)"
|
||||||
IMPORT = "import", "Import hors UE (autoliquidation)"
|
IMPORT = "import", "Import hors UE (autoliquidation)"
|
||||||
|
|
||||||
|
|
||||||
class Categorie(models.Model):
|
class Categorie(models.Model):
|
||||||
"""Catégorie globale (partagée entre entreprises).
|
"""Catégorie globale. exclure_calculs=True => mouvement neutre (hors fiscal)."""
|
||||||
|
USAGE = [("depense", "Dépense"), ("vente", "Vente"), ("neutre", "Mouvement neutre")]
|
||||||
Une catégorie « neutre » (exclure_calculs=True) sert au suivi de trésorerie
|
|
||||||
mais n'entre PAS dans les calculs fiscaux : paiement d'URSSAF, de TVA,
|
|
||||||
remboursement de TVA, prélèvement personnel, virement interne…
|
|
||||||
"""
|
|
||||||
USAGE = [
|
|
||||||
("depense", "Dépense"),
|
|
||||||
("vente", "Vente"),
|
|
||||||
("neutre", "Mouvement neutre"),
|
|
||||||
]
|
|
||||||
nom = models.CharField(max_length=100, unique=True)
|
nom = models.CharField(max_length=100, unique=True)
|
||||||
usage = models.CharField(max_length=10, choices=USAGE, default="depense")
|
usage = models.CharField(max_length=10, choices=USAGE, default="depense")
|
||||||
exclure_calculs = models.BooleanField(
|
exclure_calculs = models.BooleanField(
|
||||||
@@ -62,21 +48,14 @@ class Categorie(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Bareme(models.Model):
|
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)
|
activite = models.CharField(max_length=20, choices=Activite.choices)
|
||||||
annee = models.PositiveIntegerField()
|
annee = models.PositiveIntegerField()
|
||||||
taux_urssaf = models.DecimalField(
|
taux_urssaf = models.DecimalField("Taux cotisations URSSAF (%)", max_digits=5, decimal_places=2)
|
||||||
"Taux cotisations URSSAF (%)", max_digits=5, decimal_places=2)
|
abattement_fiscal = models.DecimalField("Abattement forfaitaire (%)", max_digits=5, decimal_places=2)
|
||||||
abattement_fiscal = models.DecimalField(
|
taux_vl = models.DecimalField("Taux versement libératoire (%)", max_digits=4, decimal_places=2)
|
||||||
"Abattement forfaitaire (%)", max_digits=5, decimal_places=2)
|
seuil_ca = models.DecimalField("Plafond CA micro (€)", max_digits=12, decimal_places=2)
|
||||||
taux_vl = models.DecimalField(
|
seuil_tva_base = models.DecimalField("Seuil franchise TVA (€)", max_digits=12, decimal_places=2)
|
||||||
"Taux versement libératoire (%)", max_digits=4, decimal_places=2)
|
seuil_tva_majore = models.DecimalField("Seuil franchise TVA majoré (€)", max_digits=12, 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:
|
class Meta:
|
||||||
verbose_name = "Barème"
|
verbose_name = "Barème"
|
||||||
@@ -95,19 +74,14 @@ class Entreprise(models.Model):
|
|||||||
activite = models.CharField(
|
activite = models.CharField(
|
||||||
"Activité principale", max_length=20, choices=Activite.choices,
|
"Activité principale", max_length=20, choices=Activite.choices,
|
||||||
default=Activite.SERVICE_BNC,
|
default=Activite.SERVICE_BNC,
|
||||||
help_text="Activité par défaut des nouvelles factures. "
|
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(
|
franchise_tva = models.BooleanField(
|
||||||
"En franchise de TVA", default=False,
|
"En franchise de TVA", default=False,
|
||||||
help_text="Décochez si vous facturez et déclarez la TVA.")
|
help_text="Décochez si vous facturez et déclarez la TVA.")
|
||||||
taux_tva_defaut = models.DecimalField(
|
taux_tva_defaut = models.DecimalField(
|
||||||
"Taux de TVA par défaut (%)", max_digits=4, decimal_places=2,
|
"Taux de TVA par défaut (%)", max_digits=4, decimal_places=2, default=Decimal("20.00"))
|
||||||
default=Decimal("20.00"))
|
versement_liberatoire = models.BooleanField("Option versement libératoire", default=False)
|
||||||
versement_liberatoire = models.BooleanField(
|
num_tva_intracom = models.CharField("N° TVA intracommunautaire", max_length=20, blank=True)
|
||||||
"Option versement libératoire", default=False)
|
|
||||||
num_tva_intracom = models.CharField(
|
|
||||||
"N° TVA intracommunautaire", max_length=20, blank=True)
|
|
||||||
vue_tva = models.CharField(
|
vue_tva = models.CharField(
|
||||||
"Mode d'affichage de la déclaration TVA", max_length=10,
|
"Mode d'affichage de la déclaration TVA", max_length=10,
|
||||||
choices=VUE_TVA, default="simplifie")
|
choices=VUE_TVA, default="simplifie")
|
||||||
@@ -128,8 +102,7 @@ class Entreprise(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Client(models.Model):
|
class Client(models.Model):
|
||||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="clients")
|
||||||
related_name="clients")
|
|
||||||
nom = models.CharField(max_length=200)
|
nom = models.CharField(max_length=200)
|
||||||
email = models.EmailField(blank=True)
|
email = models.EmailField(blank=True)
|
||||||
siret = models.CharField(max_length=20, blank=True)
|
siret = models.CharField(max_length=20, blank=True)
|
||||||
@@ -146,25 +119,18 @@ class Client(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Facture(models.Model):
|
class Facture(models.Model):
|
||||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="factures")
|
||||||
related_name="factures")
|
client = models.ForeignKey(Client, on_delete=models.PROTECT, related_name="factures")
|
||||||
client = models.ForeignKey(Client, on_delete=models.PROTECT,
|
|
||||||
related_name="factures")
|
|
||||||
numero = models.CharField(max_length=50)
|
numero = models.CharField(max_length=50)
|
||||||
activite = models.CharField(max_length=20, choices=Activite.choices)
|
activite = models.CharField(max_length=20, choices=Activite.choices)
|
||||||
date_emission = models.DateField()
|
date_emission = models.DateField()
|
||||||
date_encaissement = models.DateField(
|
date_encaissement = models.DateField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text="À renseigner une fois la facture payée. "
|
help_text="Le CA micro est compté à l'encaissement.")
|
||||||
"Le CA micro est compté à l'encaissement.")
|
statut = models.CharField(max_length=12, choices=StatutFacture.choices, default=StatutFacture.BROUILLON)
|
||||||
statut = models.CharField(max_length=12, choices=StatutFacture.choices,
|
montant_ht = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
|
||||||
default=StatutFacture.BROUILLON)
|
montant_tva = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
|
||||||
montant_ht = models.DecimalField(max_digits=12, decimal_places=2,
|
montant_ttc = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
|
||||||
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)
|
notes = models.TextField(blank=True)
|
||||||
cree_le = models.DateTimeField(auto_now_add=True)
|
cree_le = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@@ -178,7 +144,6 @@ class Facture(models.Model):
|
|||||||
return f"{self.numero} — {self.client}"
|
return f"{self.numero} — {self.client}"
|
||||||
|
|
||||||
def recompute_totaux(self, save=True):
|
def recompute_totaux(self, save=True):
|
||||||
"""Recalcule HT/TVA/TTC à partir des lignes."""
|
|
||||||
ht = Decimal("0")
|
ht = Decimal("0")
|
||||||
tva = Decimal("0")
|
tva = Decimal("0")
|
||||||
for ligne in self.lignes.all():
|
for ligne in self.lignes.all():
|
||||||
@@ -193,14 +158,11 @@ class Facture(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class LigneFacture(models.Model):
|
class LigneFacture(models.Model):
|
||||||
facture = models.ForeignKey(Facture, on_delete=models.CASCADE,
|
facture = models.ForeignKey(Facture, on_delete=models.CASCADE, related_name="lignes")
|
||||||
related_name="lignes")
|
|
||||||
designation = models.CharField(max_length=255)
|
designation = models.CharField(max_length=255)
|
||||||
quantite = models.DecimalField(max_digits=12, decimal_places=3,
|
quantite = models.DecimalField(max_digits=12, decimal_places=3, default=Decimal("1"))
|
||||||
default=Decimal("1"))
|
|
||||||
prix_unitaire_ht = models.DecimalField(max_digits=12, decimal_places=2)
|
prix_unitaire_ht = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
taux_tva = models.DecimalField(max_digits=4, decimal_places=2,
|
taux_tva = models.DecimalField(max_digits=4, decimal_places=2, default=Decimal("0"))
|
||||||
default=Decimal("0"))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Ligne de facture"
|
verbose_name = "Ligne de facture"
|
||||||
@@ -215,15 +177,17 @@ class LigneFacture(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Paiement(models.Model):
|
class Paiement(models.Model):
|
||||||
"""Opération bancaire réelle, à éclater en une ou plusieurs dépenses
|
"""Opération bancaire réelle, à éclater en une ou plusieurs dépenses."""
|
||||||
(ex. un débit Amazon regroupant plusieurs vendeurs/factures)."""
|
|
||||||
SENS = [("debit", "Débit (achat)"), ("credit", "Crédit (encaissement)")]
|
SENS = [("debit", "Débit (achat)"), ("credit", "Crédit (encaissement)")]
|
||||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="paiements")
|
||||||
related_name="paiements")
|
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
libelle = models.CharField("Libellé bancaire", max_length=255)
|
libelle = models.CharField("Libellé bancaire", max_length=255)
|
||||||
montant_total = models.DecimalField(max_digits=12, decimal_places=2)
|
montant_total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
sens = models.CharField(max_length=6, choices=SENS, default="debit")
|
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 "
|
||||||
|
"(remboursement TVA, virement…).")
|
||||||
cree_le = models.DateTimeField(auto_now_add=True)
|
cree_le = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -249,32 +213,27 @@ class Paiement(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Depense(models.Model):
|
class Depense(models.Model):
|
||||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="depenses")
|
||||||
related_name="depenses")
|
paiement = models.ForeignKey(Paiement, on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
paiement = models.ForeignKey(Paiement, on_delete=models.SET_NULL,
|
related_name="depenses",
|
||||||
null=True, blank=True, related_name="depenses",
|
help_text="Opération bancaire dont cette dépense fait partie.")
|
||||||
help_text="Opération bancaire dont cette dépense "
|
|
||||||
"fait partie (achat groupé éclaté).")
|
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
activite = models.CharField(
|
activite = models.CharField(
|
||||||
max_length=20, choices=Activite.choices, blank=True,
|
max_length=20, choices=Activite.choices, blank=True,
|
||||||
help_text="Activité concernée (facultatif, pour la ventilation).")
|
help_text="Activité concernée (facultatif, pour la ventilation).")
|
||||||
categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL,
|
categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
null=True, blank=True, related_name="depenses")
|
related_name="depenses")
|
||||||
libelle = models.CharField(max_length=255)
|
libelle = models.CharField(max_length=255)
|
||||||
montant_ttc = models.DecimalField(
|
montant_ttc = models.DecimalField(
|
||||||
"Montant payé (€)", max_digits=12, decimal_places=2,
|
"Montant payé (€)", max_digits=12, decimal_places=2,
|
||||||
help_text="Pour un achat UE/import, c'est le montant HT (sans TVA facturée).")
|
help_text="Pour un achat UE/import, c'est le montant HT (sans TVA facturée).")
|
||||||
montant_tva = models.DecimalField(
|
montant_tva = models.DecimalField(
|
||||||
"TVA déductible facturée (€)", max_digits=12, decimal_places=2,
|
"TVA déductible facturée (€)", max_digits=12, decimal_places=2, default=Decimal("0"),
|
||||||
default=Decimal("0"),
|
|
||||||
help_text="TVA payée au fournisseur (achats nationaux uniquement).")
|
help_text="TVA payée au fournisseur (achats nationaux uniquement).")
|
||||||
regime_tva = models.CharField(
|
regime_tva = models.CharField(
|
||||||
"Régime de TVA", max_length=12, choices=RegimeTva.choices,
|
"Régime de TVA", max_length=12, choices=RegimeTva.choices, default=RegimeTva.NATIONALE)
|
||||||
default=RegimeTva.NATIONALE)
|
|
||||||
taux_tva = models.DecimalField(
|
taux_tva = models.DecimalField(
|
||||||
"Taux TVA autoliquidation (%)", max_digits=4, decimal_places=2,
|
"Taux TVA autoliquidation (%)", max_digits=4, decimal_places=2, default=Decimal("0"),
|
||||||
default=Decimal("0"),
|
|
||||||
help_text="Taux français à auto-liquider pour les achats UE/import (ex. 20).")
|
help_text="Taux français à auto-liquider pour les achats UE/import (ex. 20).")
|
||||||
cree_le = models.DateTimeField(auto_now_add=True)
|
cree_le = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@@ -288,33 +247,26 @@ class Depense(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def neutre(self):
|
def neutre(self):
|
||||||
"""Mouvement neutre : exclu des calculs fiscaux."""
|
|
||||||
return bool(self.categorie and self.categorie.exclure_calculs)
|
return bool(self.categorie and self.categorie.exclure_calculs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def autoliquidee(self):
|
def autoliquidee(self):
|
||||||
"""True si la TVA est auto-liquidée (achat UE ou import)."""
|
|
||||||
return self.regime_tva in (RegimeTva.INTRACOM, RegimeTva.IMPORT)
|
return self.regime_tva in (RegimeTva.INTRACOM, RegimeTva.IMPORT)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def montant_ht(self):
|
def montant_ht(self):
|
||||||
# En autoliquidation, le fournisseur ne facture pas de TVA : HT = montant payé.
|
|
||||||
if self.autoliquidee:
|
if self.autoliquidee:
|
||||||
return self.montant_ttc.quantize(Decimal("0.01"))
|
return self.montant_ttc.quantize(Decimal("0.01"))
|
||||||
return (self.montant_ttc - self.montant_tva).quantize(Decimal("0.01"))
|
return (self.montant_ttc - self.montant_tva).quantize(Decimal("0.01"))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tva_autoliquidee(self):
|
def tva_autoliquidee(self):
|
||||||
"""TVA française auto-liquidée (comptée en collectée ET en déductible)."""
|
|
||||||
if self.autoliquidee and not self.neutre:
|
if self.autoliquidee and not self.neutre:
|
||||||
return (self.montant_ttc * self.taux_tva / Decimal("100")).quantize(
|
return (self.montant_ttc * self.taux_tva / Decimal("100")).quantize(Decimal("0.01"))
|
||||||
Decimal("0.01"))
|
|
||||||
return Decimal("0.00")
|
return Decimal("0.00")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tva_deductible_value(self):
|
def tva_deductible_value(self):
|
||||||
"""TVA récupérable : facturée (national) ou auto-liquidée (UE/import).
|
|
||||||
Nulle pour un mouvement neutre."""
|
|
||||||
if self.neutre:
|
if self.neutre:
|
||||||
return Decimal("0.00")
|
return Decimal("0.00")
|
||||||
if self.autoliquidee:
|
if self.autoliquidee:
|
||||||
|
|||||||
+182
-94
@@ -1,96 +1,110 @@
|
|||||||
"""
|
"""
|
||||||
Logique fiscale — JB Compta.
|
Logique fiscale — JB Compta.
|
||||||
|
|
||||||
Bases de calcul : CA HORS TAXES (HT) encaissé.
|
Bases : CA HORS TAXES (HT) encaissé.
|
||||||
- Base des cotisations URSSAF et du versement libératoire (assujetti TVA).
|
TVA collectée = TVA ventes + TVA auto-liquidée (UE/import).
|
||||||
- En franchise de TVA, HT = TTC : le calcul reste correct.
|
TVA déductible = TVA achats nationaux + TVA auto-liquidée. Neutres exclus.
|
||||||
|
|
||||||
Multi-activité : une entreprise peut exercer plusieurs activités (ex. Vente BIC
|
|
||||||
+ Prestation BIC). Taux URSSAF, abattements, taux VL et seuils diffèrent par
|
|
||||||
activité, donc tout est ventilé par activité puis totalisé.
|
|
||||||
|
|
||||||
TVA :
|
|
||||||
- TVA collectée = TVA sur ventes encaissées + TVA auto-liquidée (achats UE/import).
|
|
||||||
- TVA déductible = TVA facturée sur achats nationaux + TVA auto-liquidée.
|
|
||||||
- L'autoliquidation s'ajoute aux deux côtés (effet net souvent nul), mais doit
|
|
||||||
apparaître sur la CA3 : on l'expose donc séparément.
|
|
||||||
"""
|
"""
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.db.models.functions import TruncMonth
|
from django.db.models.functions import TruncMonth
|
||||||
|
|
||||||
from .models import Facture, Depense, Bareme, StatutFacture, Activite
|
from .models import (Facture, Depense, LigneFacture, Bareme, StatutFacture,
|
||||||
|
Activite, RegimeTva)
|
||||||
|
|
||||||
CENT = Decimal("0.01")
|
CENT = Decimal("0.01")
|
||||||
|
MOIS_FR = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet",
|
||||||
|
"Août", "Septembre", "Octobre", "Novembre", "Décembre"]
|
||||||
|
|
||||||
|
|
||||||
def _q(value):
|
def _q(v):
|
||||||
return (value or Decimal("0")).quantize(CENT)
|
return (v or Decimal("0")).quantize(CENT)
|
||||||
|
|
||||||
|
|
||||||
def factures_encaissees(entreprise, annee):
|
# ---- Sélections de période ---------------------------------------------
|
||||||
return Facture.objects.filter(
|
|
||||||
entreprise=entreprise,
|
def factures_encaissees(entreprise, annee, mois=None):
|
||||||
statut=StatutFacture.ENCAISSEE,
|
qs = Facture.objects.filter(entreprise=entreprise, statut=StatutFacture.ENCAISSEE,
|
||||||
date_encaissement__year=annee,
|
date_encaissement__year=annee)
|
||||||
)
|
if mois:
|
||||||
|
qs = qs.filter(date_encaissement__month=mois)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
def depenses_annee(entreprise, annee):
|
def depenses_periode(entreprise, annee, mois=None):
|
||||||
return Depense.objects.filter(entreprise=entreprise, date__year=annee)
|
qs = Depense.objects.filter(entreprise=entreprise, date__year=annee)
|
||||||
|
if mois:
|
||||||
|
qs = qs.filter(date__month=mois)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
def ca_ht_par_activite(entreprise, annee):
|
# ---- CA -----------------------------------------------------------------
|
||||||
"""Dict {activite: CA HT encaissé}."""
|
|
||||||
rows = (
|
def ca_ht_par_activite(entreprise, annee, mois=None):
|
||||||
factures_encaissees(entreprise, annee)
|
rows = factures_encaissees(entreprise, annee, mois).values("activite").annotate(
|
||||||
.values("activite")
|
total=Sum("montant_ht"))
|
||||||
.annotate(total=Sum("montant_ht"))
|
|
||||||
)
|
|
||||||
return {r["activite"]: _q(r["total"]) for r in rows}
|
return {r["activite"]: _q(r["total"]) for r in rows}
|
||||||
|
|
||||||
|
|
||||||
def ca_ht(entreprise, annee):
|
def ca_ht(entreprise, annee, mois=None):
|
||||||
agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_ht"))
|
return _q(factures_encaissees(entreprise, annee, mois).aggregate(
|
||||||
return _q(agg["s"])
|
s=Sum("montant_ht"))["s"])
|
||||||
|
|
||||||
|
|
||||||
# ---- TVA ----------------------------------------------------------------
|
# ---- TVA ----------------------------------------------------------------
|
||||||
|
|
||||||
def tva_sur_ventes(entreprise, annee):
|
def tva_sur_ventes(entreprise, annee, mois=None):
|
||||||
agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_tva"))
|
return _q(factures_encaissees(entreprise, annee, mois).aggregate(
|
||||||
return _q(agg["s"])
|
s=Sum("montant_tva"))["s"])
|
||||||
|
|
||||||
|
|
||||||
def tva_autoliquidee(entreprise, annee):
|
def tva_autoliquidee(entreprise, annee, mois=None):
|
||||||
"""TVA auto-liquidée sur les achats UE/import (intracom + import)."""
|
total = sum((d.tva_autoliquidee for d in depenses_periode(entreprise, annee, mois)),
|
||||||
total = sum((d.tva_autoliquidee for d in depenses_annee(entreprise, annee)),
|
|
||||||
Decimal("0"))
|
Decimal("0"))
|
||||||
return _q(total)
|
return _q(total)
|
||||||
|
|
||||||
|
|
||||||
def tva_deductible(entreprise, annee):
|
def tva_deductible(entreprise, annee, mois=None):
|
||||||
"""TVA récupérable : facturée (national) + auto-liquidée (UE/import)."""
|
total = sum((d.tva_deductible_value for d in depenses_periode(entreprise, annee, mois)),
|
||||||
total = sum((d.tva_deductible_value for d in depenses_annee(entreprise, annee)),
|
|
||||||
Decimal("0"))
|
Decimal("0"))
|
||||||
return _q(total)
|
return _q(total)
|
||||||
|
|
||||||
|
|
||||||
def tva_collectee(entreprise, annee):
|
def tva_collectee(entreprise, annee, mois=None):
|
||||||
"""TVA due : sur ventes + auto-liquidée sur achats UE/import."""
|
return _q(tva_sur_ventes(entreprise, annee, mois)
|
||||||
return _q(tva_sur_ventes(entreprise, annee) + tva_autoliquidee(entreprise, annee))
|
+ tva_autoliquidee(entreprise, annee, mois))
|
||||||
|
|
||||||
|
|
||||||
# ---- CA mensuel ---------------------------------------------------------
|
def tva_par_taux(entreprise, annee, mois=None):
|
||||||
|
"""Ventilation des ventes encaissées par taux de TVA : {taux: (base_ht, tva)}."""
|
||||||
|
lignes = LigneFacture.objects.filter(
|
||||||
|
facture__in=factures_encaissees(entreprise, annee, mois))
|
||||||
|
par_taux = {}
|
||||||
|
for l in lignes.select_related("facture"):
|
||||||
|
base = l.montant_ht
|
||||||
|
taux = l.taux_tva
|
||||||
|
b, t = par_taux.get(taux, (Decimal("0"), Decimal("0")))
|
||||||
|
par_taux[taux] = (b + base, t + base * taux / Decimal("100"))
|
||||||
|
return {taux: (_q(b), _q(t)) for taux, (b, t) in sorted(par_taux.items())}
|
||||||
|
|
||||||
|
|
||||||
|
def intracom_periode(entreprise, annee, mois=None):
|
||||||
|
"""Acquisitions UE/import auto-liquidées : (base_ht, tva)."""
|
||||||
|
base = Decimal("0")
|
||||||
|
tva = Decimal("0")
|
||||||
|
for d in depenses_periode(entreprise, annee, mois):
|
||||||
|
if d.autoliquidee and not d.neutre:
|
||||||
|
base += d.montant_ht
|
||||||
|
tva += d.tva_autoliquidee
|
||||||
|
return _q(base), _q(tva)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- CA mensuel (graphe) ------------------------------------------------
|
||||||
|
|
||||||
def ca_ht_mensuel(entreprise, annee):
|
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)
|
||||||
rows = (
|
|
||||||
factures_encaissees(entreprise, annee)
|
|
||||||
.annotate(mois=TruncMonth("date_encaissement"))
|
.annotate(mois=TruncMonth("date_encaissement"))
|
||||||
.values("mois")
|
.values("mois").annotate(total=Sum("montant_ht")))
|
||||||
.annotate(total=Sum("montant_ht"))
|
|
||||||
)
|
|
||||||
mensuel = [Decimal("0")] * 12
|
mensuel = [Decimal("0")] * 12
|
||||||
for r in rows:
|
for r in rows:
|
||||||
if r["mois"]:
|
if r["mois"]:
|
||||||
@@ -101,7 +115,6 @@ def ca_ht_mensuel(entreprise, annee):
|
|||||||
# ---- Détail par activité ------------------------------------------------
|
# ---- Détail par activité ------------------------------------------------
|
||||||
|
|
||||||
def detail_activite(activite, ca, bareme):
|
def detail_activite(activite, ca, bareme):
|
||||||
"""Calculs fiscaux pour une activité donnée."""
|
|
||||||
label = dict(Activite.choices).get(activite, activite)
|
label = dict(Activite.choices).get(activite, activite)
|
||||||
d = {"activite": activite, "label": label, "ca_ht": ca, "bareme": bareme}
|
d = {"activite": activite, "label": label, "ca_ht": ca, "bareme": bareme}
|
||||||
if bareme:
|
if bareme:
|
||||||
@@ -111,72 +124,147 @@ def detail_activite(activite, ca, bareme):
|
|||||||
"taux_vl": bareme.taux_vl,
|
"taux_vl": bareme.taux_vl,
|
||||||
"versement_liberatoire": _q(ca * bareme.taux_vl / Decimal("100")),
|
"versement_liberatoire": _q(ca * bareme.taux_vl / Decimal("100")),
|
||||||
"abattement_fiscal": bareme.abattement_fiscal,
|
"abattement_fiscal": bareme.abattement_fiscal,
|
||||||
"revenu_imposable": _q(
|
"revenu_imposable": _q(ca * (Decimal("100") - bareme.abattement_fiscal) / Decimal("100")),
|
||||||
ca * (Decimal("100") - bareme.abattement_fiscal) / Decimal("100")),
|
|
||||||
"seuil_ca": bareme.seuil_ca,
|
"seuil_ca": bareme.seuil_ca,
|
||||||
"pct_seuil_ca": (_q(ca / bareme.seuil_ca * Decimal("100"))
|
"pct_seuil_ca": (_q(ca / bareme.seuil_ca * Decimal("100")) if bareme.seuil_ca else Decimal("0")),
|
||||||
if bareme.seuil_ca else Decimal("0")),
|
|
||||||
"seuil_tva_base": bareme.seuil_tva_base,
|
"seuil_tva_base": bareme.seuil_tva_base,
|
||||||
"depasse_seuil_tva": ca > bareme.seuil_tva_base,
|
"depasse_seuil_tva": ca > bareme.seuil_tva_base,
|
||||||
"depasse_seuil_ca": ca > bareme.seuil_ca,
|
"depasse_seuil_ca": ca > bareme.seuil_ca,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
d.update({
|
for k in ("taux_urssaf", "cotisations", "taux_vl", "versement_liberatoire",
|
||||||
"taux_urssaf": None, "cotisations": None,
|
"abattement_fiscal", "revenu_imposable", "seuil_ca", "pct_seuil_ca",
|
||||||
"taux_vl": None, "versement_liberatoire": None,
|
"seuil_tva_base"):
|
||||||
"abattement_fiscal": None, "revenu_imposable": None,
|
d[k] = None
|
||||||
"seuil_ca": None, "pct_seuil_ca": None,
|
d["depasse_seuil_tva"] = d["depasse_seuil_ca"] = False
|
||||||
"seuil_tva_base": None, "depasse_seuil_tva": False,
|
|
||||||
"depasse_seuil_ca": False,
|
|
||||||
})
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def synthese(entreprise, annee):
|
def _lignes_activite(entreprise, annee, mois=None):
|
||||||
"""Tableau de bord chiffré, ventilé par activité puis totalisé."""
|
ca_par = ca_ht_par_activite(entreprise, annee, mois)
|
||||||
ca_par_act = ca_ht_par_activite(entreprise, annee)
|
activites = set(ca_par) | {entreprise.activite}
|
||||||
|
|
||||||
activites = set(ca_par_act) | {entreprise.activite}
|
|
||||||
baremes = {b.activite: b for b in Bareme.objects.filter(annee=annee)}
|
baremes = {b.activite: b for b in Bareme.objects.filter(annee=annee)}
|
||||||
|
return [detail_activite(a, ca_par.get(a, Decimal("0")), baremes.get(a))
|
||||||
|
for a in sorted(activites)]
|
||||||
|
|
||||||
lignes = []
|
|
||||||
for act in sorted(activites):
|
|
||||||
ca = ca_par_act.get(act, Decimal("0"))
|
|
||||||
lignes.append(detail_activite(act, ca, baremes.get(act)))
|
|
||||||
|
|
||||||
def somme(cle):
|
def _somme(lignes, cle):
|
||||||
vals = [l[cle] for l in lignes if l.get(cle) is not None]
|
vals = [l[cle] for l in lignes if l.get(cle) is not None]
|
||||||
return _q(sum(vals, Decimal("0"))) if vals else None
|
return _q(sum(vals, Decimal("0"))) if vals else None
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Vue globale (annuelle) --------------------------------------------
|
||||||
|
|
||||||
|
def synthese(entreprise, annee):
|
||||||
|
lignes = _lignes_activite(entreprise, annee)
|
||||||
tva_ventes = tva_sur_ventes(entreprise, annee)
|
tva_ventes = tva_sur_ventes(entreprise, annee)
|
||||||
tva_autoliq = tva_autoliquidee(entreprise, annee)
|
tva_autoliq = tva_autoliquidee(entreprise, annee)
|
||||||
tva_ded = tva_deductible(entreprise, annee)
|
tva_ded = tva_deductible(entreprise, annee)
|
||||||
tva_col = _q(tva_ventes + tva_autoliq)
|
tva_col = _q(tva_ventes + tva_autoliq)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"entreprise": entreprise,
|
"entreprise": entreprise, "annee": annee, "lignes_activite": lignes,
|
||||||
"annee": annee,
|
|
||||||
"lignes_activite": lignes,
|
|
||||||
"bareme_manquant": any(l["bareme"] is None and l["ca_ht"] for l in lignes),
|
"bareme_manquant": any(l["bareme"] is None and l["ca_ht"] for l in lignes),
|
||||||
"ca_ht": ca_ht(entreprise, annee),
|
"ca_ht": ca_ht(entreprise, annee),
|
||||||
"cotisations_urssaf": somme("cotisations"),
|
"cotisations_urssaf": _somme(lignes, "cotisations"),
|
||||||
"versement_liberatoire": (
|
"versement_liberatoire": (_somme(lignes, "versement_liberatoire")
|
||||||
somme("versement_liberatoire") if entreprise.versement_liberatoire else None),
|
if entreprise.versement_liberatoire else None),
|
||||||
"revenu_imposable": somme("revenu_imposable"),
|
"revenu_imposable": _somme(lignes, "revenu_imposable"),
|
||||||
"tva_sur_ventes": tva_ventes,
|
"tva_sur_ventes": tva_ventes, "tva_autoliquidee": tva_autoliq,
|
||||||
"tva_autoliquidee": tva_autoliq,
|
"tva_collectee": tva_col, "tva_deductible": tva_ded,
|
||||||
"tva_collectee": tva_col,
|
|
||||||
"tva_deductible": tva_ded,
|
|
||||||
"tva_a_reverser": _q(tva_col - tva_ded),
|
"tva_a_reverser": _q(tva_col - tva_ded),
|
||||||
"ca_mensuel": ca_ht_mensuel(entreprise, annee),
|
"ca_mensuel": ca_ht_mensuel(entreprise, annee),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Vue mensuelle ------------------------------------------------------
|
||||||
|
|
||||||
|
def tableau_mensuel(entreprise, annee):
|
||||||
|
baremes = {b.activite: b for b in Bareme.objects.filter(annee=annee)}
|
||||||
|
rows = []
|
||||||
|
for m in range(1, 13):
|
||||||
|
ca_par = ca_ht_par_activite(entreprise, annee, m)
|
||||||
|
cot = Decimal("0")
|
||||||
|
vl = Decimal("0")
|
||||||
|
for act, ca in ca_par.items():
|
||||||
|
b = baremes.get(act)
|
||||||
|
if b:
|
||||||
|
cot += ca * b.taux_urssaf / Decimal("100")
|
||||||
|
vl += ca * b.taux_vl / Decimal("100")
|
||||||
|
tva_col = tva_collectee(entreprise, annee, m)
|
||||||
|
tva_ded = tva_deductible(entreprise, annee, m)
|
||||||
|
rows.append({
|
||||||
|
"mois": m, "nom": MOIS_FR[m - 1],
|
||||||
|
"ca_ht": ca_ht(entreprise, annee, m),
|
||||||
|
"cotisations": _q(cot),
|
||||||
|
"versement_liberatoire": _q(vl) if entreprise.versement_liberatoire else None,
|
||||||
|
"tva_collectee": tva_col, "tva_deductible": tva_ded,
|
||||||
|
"tva_a_reverser": _q(tva_col - tva_ded),
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Déclaration URSSAF (période) --------------------------------------
|
||||||
|
|
||||||
|
def declaration_urssaf(entreprise, annee, mois=None):
|
||||||
|
lignes = _lignes_activite(entreprise, annee, mois)
|
||||||
|
return {
|
||||||
|
"entreprise": entreprise, "annee": annee, "mois": mois,
|
||||||
|
"mois_nom": MOIS_FR[mois - 1] if mois else "Année entière",
|
||||||
|
"lignes": lignes,
|
||||||
|
"ca_ht": ca_ht(entreprise, annee, mois),
|
||||||
|
"cotisations": _somme(lignes, "cotisations"),
|
||||||
|
"versement_liberatoire": (_somme(lignes, "versement_liberatoire")
|
||||||
|
if entreprise.versement_liberatoire else None),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Déclaration TVA (période) -----------------------------------------
|
||||||
|
|
||||||
|
def declaration_tva(entreprise, annee, mois=None):
|
||||||
|
par_taux = tva_par_taux(entreprise, annee, mois)
|
||||||
|
intra_base, intra_tva = intracom_periode(entreprise, annee, mois)
|
||||||
|
tva_ventes = sum((t for _, t in par_taux.values()), Decimal("0"))
|
||||||
|
tva_ded = tva_deductible(entreprise, annee, mois)
|
||||||
|
tva_brute = _q(tva_ventes + intra_tva)
|
||||||
|
a_payer = _q(tva_brute - tva_ded)
|
||||||
|
|
||||||
|
def couple(taux):
|
||||||
|
return par_taux.get(Decimal(taux), (Decimal("0.00"), Decimal("0.00")))
|
||||||
|
|
||||||
|
b20, t20 = couple("20.00")
|
||||||
|
b10, t10 = couple("10.00")
|
||||||
|
b55, t55 = couple("5.50")
|
||||||
|
base_taxable = _q(sum((b for b, _ in par_taux.values()), Decimal("0")))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"entreprise": entreprise, "annee": annee, "mois": mois,
|
||||||
|
"mois_nom": MOIS_FR[mois - 1] if mois else "Année entière",
|
||||||
|
"par_taux": [{"taux": k, "base": v[0], "tva": v[1]} for k, v in par_taux.items()],
|
||||||
|
# Résumé simplifié
|
||||||
|
"tva_sur_ventes": _q(tva_ventes),
|
||||||
|
"tva_autoliquidee": intra_tva,
|
||||||
|
"tva_collectee": tva_brute,
|
||||||
|
"tva_deductible": tva_ded,
|
||||||
|
"tva_a_payer": a_payer,
|
||||||
|
"credit_tva": _q(-a_payer) if a_payer < 0 else Decimal("0.00"),
|
||||||
|
# Champs CA3
|
||||||
|
"ca3": {
|
||||||
|
"l01_base_taxable": base_taxable,
|
||||||
|
"l08_base": b20, "l08_tva": t20,
|
||||||
|
"l9B_base": b10, "l9B_tva": t10,
|
||||||
|
"l09_base": b55, "l09_tva": t55,
|
||||||
|
"l03_intra_base": intra_base,
|
||||||
|
"l17_intra_tva": intra_tva,
|
||||||
|
"l16_tva_brute": tva_brute,
|
||||||
|
"l20_tva_deductible": tva_ded,
|
||||||
|
"l28_total_deductible": tva_ded,
|
||||||
|
"l32_a_payer": a_payer if a_payer > 0 else Decimal("0.00"),
|
||||||
|
"l25_credit": _q(-a_payer) if a_payer < 0 else Decimal("0.00"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def annees_disponibles(entreprise):
|
def annees_disponibles(entreprise):
|
||||||
qs = (
|
qs = Facture.objects.filter(entreprise=entreprise, statut=StatutFacture.ENCAISSEE,
|
||||||
Facture.objects.filter(entreprise=entreprise,
|
date_encaissement__isnull=False).dates("date_encaissement", "year")
|
||||||
statut=StatutFacture.ENCAISSEE,
|
|
||||||
date_encaissement__isnull=False)
|
|
||||||
.dates("date_encaissement", "year")
|
|
||||||
)
|
|
||||||
return sorted({d.year for d in qs}, reverse=True)
|
return sorted({d.year for d in qs}, reverse=True)
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>JB Compta — {% block titre %}Tableau de bord{% endblock %}</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); text-decoration:none; }
|
||||||
|
.sub { color:var(--muted); margin-bottom:16px; }
|
||||||
|
.nav { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
|
||||||
|
.nav a { padding:7px 13px; border-radius:8px; border:1px solid #334155;
|
||||||
|
background:var(--card); font-size:14px; }
|
||||||
|
.nav a.on { background:var(--accent); color:#04263a; border-color:var(--accent); font-weight:600; }
|
||||||
|
.nav a.saisie { margin-left:auto; }
|
||||||
|
form.filters { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:20px; }
|
||||||
|
select, button, input { 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(190px,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:24px; 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;} }
|
||||||
|
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; }
|
||||||
|
.exports a { display:inline-block; margin-right:12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{% if not entreprise %}
|
||||||
|
<h1>Aucune entreprise</h1>
|
||||||
|
<p class="sub">Crée une entreprise dans <a href="/admin/">l'administration</a>,
|
||||||
|
ou <a href="/saisie/">saisis une opération</a>.</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="nav">
|
||||||
|
<a class="{% if vue == 'global' %}on{% endif %}" href="/dashboard/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Global</a>
|
||||||
|
<a class="{% if vue == 'mensuel' %}on{% endif %}" href="/dashboard/mensuel/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Mensuel</a>
|
||||||
|
<a class="{% if vue == 'urssaf' %}on{% endif %}" href="/declaration/urssaf/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Déclaration URSSAF</a>
|
||||||
|
<a class="{% if vue == 'tva' %}on{% endif %}" href="/declaration/tva/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Déclaration TVA</a>
|
||||||
|
<a class="saisie" href="/saisie/">+ Saisir</a>
|
||||||
|
<a href="/admin/">Admin</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
{% block extrafilters %}{% endblock %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,86 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "compta/base.html" %}
|
||||||
<html lang="fr">
|
{% block titre %}Global{% endblock %}
|
||||||
<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="/saisie/"><button type="button">+ Saisir un paiement</button></a>
|
|
||||||
<a href="/admin/"><button type="button">Admin</button></a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
{% if bareme_manquant %}
|
{% if bareme_manquant %}
|
||||||
<div class="card bad" style="margin-bottom:16px;">
|
<div class="card bad" style="margin-bottom:16px;">
|
||||||
Une activité avec du CA n'a pas de barème {{ annee }}. Ajoute-le dans
|
Une activité avec du CA n'a pas de barème {{ annee }}.
|
||||||
<a href="/admin/compta/bareme/">Barèmes</a>.
|
Ajoute-le dans <a href="/admin/compta/bareme/">Barèmes</a>.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Totaux entreprise -->
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">CA HT encaissé (total)</div>
|
<div class="label">CA HT encaissé (total)</div>
|
||||||
<div class="value">{{ ca_ht }} €</div>
|
<div class="value">{{ ca_ht }} €</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">Cotisations URSSAF (total)</div>
|
<div class="label">Cotisations URSSAF</div>
|
||||||
<div class="value">{{ cotisations_urssaf|default:"—" }} €</div>
|
<div class="value">{{ cotisations_urssaf|default:"—" }} €</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -91,93 +26,55 @@
|
|||||||
<div class="label">TVA à reverser</div>
|
<div class="label">TVA à reverser</div>
|
||||||
<div class="value">{{ tva_a_reverser }} €</div>
|
<div class="value">{{ tva_a_reverser }} €</div>
|
||||||
<div class="hint">{{ tva_collectee }} collectée − {{ tva_deductible }} déductible</div>
|
<div class="hint">{{ tva_collectee }} collectée − {{ tva_deductible }} déductible</div>
|
||||||
{% if tva_autoliquidee %}<div class="hint">dont {{ tva_autoliquidee }} € auto-liquidée (UE/import)</div>{% endif %}
|
{% if tva_autoliquidee %}<div class="hint">dont {{ tva_autoliquidee }} € auto-liquidée</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ventilation par activité -->
|
|
||||||
<h2>Ventilation par activité</h2>
|
<h2>Ventilation par activité</h2>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<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>
|
||||||
<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>
|
<tbody>
|
||||||
{% for l in lignes_activite %}
|
{% for l in lignes_activite %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ l.label }}</td>
|
<td>{{ l.label }}</td>
|
||||||
<td>{{ l.ca_ht }} €</td>
|
<td>{{ l.ca_ht }} €</td>
|
||||||
<td class="{% if l.depasse_seuil_ca %}bad{% endif %}">
|
<td class="{% if l.depasse_seuil_ca %}bad{% endif %}">{% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %}</td>
|
||||||
{% 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.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.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>
|
<td>{% if l.revenu_imposable is not None %}{{ l.revenu_imposable }} €{% else %}—{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr class="total">
|
<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>
|
||||||
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Détail TVA -->
|
|
||||||
<h2>TVA</h2>
|
|
||||||
<div class="card">
|
|
||||||
<table>
|
|
||||||
<tr><td>TVA collectée sur ventes</td><td>{{ tva_sur_ventes }} €</td></tr>
|
|
||||||
<tr><td>TVA auto-liquidée (UE / import)</td><td>{{ tva_autoliquidee }} €</td></tr>
|
|
||||||
<tr><td>TVA collectée totale (due)</td><td>{{ tva_collectee }} €</td></tr>
|
|
||||||
<tr><td>TVA déductible</td><td>{{ tva_deductible }} €</td></tr>
|
|
||||||
<tr class="total"><td>TVA à reverser</td><td>{{ tva_a_reverser }} €</td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label" style="margin-bottom:8px;">CA HT mensuel (toutes activités)</div>
|
<div class="label" style="margin-bottom:8px;">CA HT mensuel</div>
|
||||||
<canvas id="caChart" height="120"></canvas>
|
<canvas id="caChart" height="120"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label" style="margin-bottom:8px;">À provisionner (année)</div>
|
<div class="label" style="margin-bottom:8px;">Exports CSV</div>
|
||||||
<table>
|
<div class="exports">
|
||||||
<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_ventes' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">Ventes</a>
|
||||||
<a href="{% url 'export_tva' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">TVA</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>
|
<a href="{% url 'export_urssaf' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">URSSAF</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const ctx = document.getElementById('caChart');
|
new Chart(document.getElementById('caChart'), {
|
||||||
new Chart(ctx, {
|
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: { labels: {{ mois_labels|safe }},
|
||||||
labels: {{ mois_labels|safe }},
|
datasets: [{ label:'CA HT (€)', data: {{ ca_mensuel_json|safe }}, backgroundColor:'#38bdf8' }] },
|
||||||
datasets: [{ label: 'CA HT (€)', data: {{ ca_mensuel_json|safe }},
|
|
||||||
backgroundColor: '#38bdf8' }]
|
|
||||||
},
|
|
||||||
options: { plugins:{legend:{display:false}},
|
options: { plugins:{legend:{display:false}},
|
||||||
scales:{ x:{ticks:{color:'#94a3b8'}}, y:{ticks:{color:'#94a3b8'}} } }
|
scales:{ x:{ticks:{color:'#94a3b8'}}, y:{ticks:{color:'#94a3b8'}} } }
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endblock %}
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{% extends "compta/base.html" %}
|
||||||
|
{% block titre %}Déclaration TVA{% endblock %}
|
||||||
|
|
||||||
|
{% block extrafilters %}
|
||||||
|
<select name="mois" onchange="this.form.submit()">
|
||||||
|
{% for num, nom in mois_liste %}
|
||||||
|
<option value="{{ num }}" {% if num == mois_sel %}selected{% endif %}>{{ nom }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Déclaration TVA — {{ decl.mois_nom }} {{ annee }}</h2>
|
||||||
|
<div class="sub">
|
||||||
|
Mode :
|
||||||
|
<a href="/declaration/tva/?entreprise={{ entreprise.pk }}&annee={{ annee }}&mois={{ mois_sel }}&mode=simplifie"
|
||||||
|
{% if mode != 'ca3' %}class="on" style="padding:2px 8px;border-radius:6px;"{% endif %}>Résumé</a> ·
|
||||||
|
<a href="/declaration/tva/?entreprise={{ entreprise.pk }}&annee={{ annee }}&mois={{ mois_sel }}&mode=ca3"
|
||||||
|
{% if mode == 'ca3' %}class="on" style="padding:2px 8px;border-radius:6px;"{% endif %}>Champs CA3</a>
|
||||||
|
(réglage par défaut de l'entreprise : {{ entreprise.get_vue_tva_display }})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if mode == 'ca3' %}
|
||||||
|
<div class="card">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Ligne CA3</th><th>Libellé</th><th>Base HT</th><th>TVA</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>01</td><td>Ventes / prestations taxables</td><td>{{ decl.ca3.l01_base_taxable }} €</td><td></td></tr>
|
||||||
|
<tr><td>08</td><td>Taux normal 20 %</td><td>{{ decl.ca3.l08_base }} €</td><td>{{ decl.ca3.l08_tva }} €</td></tr>
|
||||||
|
<tr><td>9B</td><td>Taux réduit 10 %</td><td>{{ decl.ca3.l9B_base }} €</td><td>{{ decl.ca3.l9B_tva }} €</td></tr>
|
||||||
|
<tr><td>09</td><td>Taux réduit 5,5 %</td><td>{{ decl.ca3.l09_base }} €</td><td>{{ decl.ca3.l09_tva }} €</td></tr>
|
||||||
|
<tr><td>03</td><td>Acquisitions intracommunautaires</td><td>{{ decl.ca3.l03_intra_base }} €</td><td></td></tr>
|
||||||
|
<tr><td>17</td><td>dont TVA sur acquisitions intracom</td><td></td><td>{{ decl.ca3.l17_intra_tva }} €</td></tr>
|
||||||
|
<tr class="total"><td>16</td><td>TVA brute due</td><td></td><td>{{ decl.ca3.l16_tva_brute }} €</td></tr>
|
||||||
|
<tr><td>20</td><td>TVA déductible (biens et services)</td><td></td><td>{{ decl.ca3.l20_tva_deductible }} €</td></tr>
|
||||||
|
<tr class="total"><td>28</td><td>Total TVA déductible</td><td></td><td>{{ decl.ca3.l28_total_deductible }} €</td></tr>
|
||||||
|
<tr class="total"><td>32</td><td>TVA à payer</td><td></td><td>{{ decl.ca3.l32_a_payer }} €</td></tr>
|
||||||
|
{% if decl.ca3.l25_credit %}<tr class="total"><td>25</td><td>Crédit de TVA</td><td></td><td>{{ decl.ca3.l25_credit }} €</td></tr>{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="sub">Numéros de lignes du formulaire 3310-CA3 (régime réel normal). À recouper
|
||||||
|
avec ta déclaration officielle — vérifie notamment la ventilation par taux.</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card"><div class="label">TVA collectée sur ventes</div><div class="value">{{ decl.tva_sur_ventes }} €</div></div>
|
||||||
|
<div class="card"><div class="label">TVA auto-liquidée (UE/import)</div><div class="value">{{ decl.tva_autoliquidee }} €</div></div>
|
||||||
|
<div class="card"><div class="label">TVA collectée totale</div><div class="value">{{ decl.tva_collectee }} €</div></div>
|
||||||
|
<div class="card"><div class="label">TVA déductible</div><div class="value">{{ decl.tva_deductible }} €</div></div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">{% if decl.credit_tva %}Crédit de TVA{% else %}TVA à payer{% endif %}</div>
|
||||||
|
<div class="value {% if decl.credit_tva %}ok{% endif %}">{% if decl.credit_tva %}{{ decl.credit_tva }}{% else %}{{ decl.tva_a_payer }}{% endif %} €</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if decl.par_taux %}
|
||||||
|
<h2>Détail par taux (ventes)</h2>
|
||||||
|
<div class="card">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Taux</th><th>Base HT</th><th>TVA</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for t in decl.par_taux %}<tr><td>{{ t.taux }} %</td><td>{{ t.base }} €</td><td>{{ t.tva }} €</td></tr>{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends "compta/base.html" %}
|
||||||
|
{% block titre %}Déclaration URSSAF{% endblock %}
|
||||||
|
|
||||||
|
{% block extrafilters %}
|
||||||
|
<select name="mois" onchange="this.form.submit()">
|
||||||
|
{% for num, nom in mois_liste %}
|
||||||
|
<option value="{{ num }}" {% if num == mois_sel %}selected{% endif %}>{{ nom }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Déclaration URSSAF — {{ decl.mois_nom }} {{ annee }}</h2>
|
||||||
|
<div class="sub">À déclarer sur autoentrepreneur.urssaf.fr : le CA HT encaissé de la période,
|
||||||
|
par type d'activité. Les cotisations (et l'impôt si versement libératoire) en découlent.</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Activité</th><th>CA HT à déclarer</th><th>Taux URSSAF</th><th>Cotisations</th>
|
||||||
|
{% if decl.versement_liberatoire is not None %}<th>Taux VL</th><th>Impôt (VL)</th>{% endif %}</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for l in decl.lignes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ l.label }}</td>
|
||||||
|
<td>{{ l.ca_ht }} €</td>
|
||||||
|
<td>{% if l.taux_urssaf is not None %}{{ l.taux_urssaf }} %{% else %}—{% endif %}</td>
|
||||||
|
<td>{% if l.cotisations is not None %}{{ l.cotisations }} €{% else %}—{% endif %}</td>
|
||||||
|
{% if decl.versement_liberatoire is not None %}
|
||||||
|
<td>{% if l.taux_vl is not None %}{{ l.taux_vl }} %{% else %}—{% endif %}</td>
|
||||||
|
<td>{% if l.versement_liberatoire is not None %}{{ l.versement_liberatoire }} €{% else %}—{% endif %}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr class="total">
|
||||||
|
<td>Total</td><td>{{ decl.ca_ht }} €</td><td></td><td>{{ decl.cotisations|default:"—" }} €</td>
|
||||||
|
{% if decl.versement_liberatoire is not None %}<td></td><td>{{ decl.versement_liberatoire }} €</td>{% endif %}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{% extends "compta/base.html" %}
|
||||||
|
{% block titre %}Mensuel{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Vue mensuelle {{ annee }}</h2>
|
||||||
|
<div class="card">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Mois</th><th>CA HT</th><th>Cotisations URSSAF</th>
|
||||||
|
<th>Impôt (VL)</th><th>TVA collectée</th><th>TVA déductible</th><th>TVA à reverser</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in rows %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ r.nom }}</td>
|
||||||
|
<td>{{ r.ca_ht }} €</td>
|
||||||
|
<td>{{ r.cotisations }} €</td>
|
||||||
|
<td>{% if r.versement_liberatoire is not None %}{{ r.versement_liberatoire }} €{% else %}—{% endif %}</td>
|
||||||
|
<td>{{ r.tva_collectee }} €</td>
|
||||||
|
<td>{{ r.tva_deductible }} €</td>
|
||||||
|
<td>{{ r.tva_a_reverser }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>JB Compta — Saisie d'un paiement</title>
|
<title>JB Compta — Saisie</title>
|
||||||
<style>
|
<style>
|
||||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
||||||
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
|
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
input, select { background:#0f172a; color:var(--txt); border:1px solid #334155;
|
input, select { background:#0f172a; color:var(--txt); border:1px solid #334155;
|
||||||
border-radius:8px; padding:8px 10px; font-size:14px; width:100%; }
|
border-radius:8px; padding:8px 10px; font-size:14px; width:100%; }
|
||||||
.head { display:grid; grid-template-columns:1fr 1fr 2fr 1fr; gap:12px; }
|
.head { display:grid; grid-template-columns:1fr 1fr 2fr 1fr; gap:12px; }
|
||||||
@media(max-width:700px){ .head{grid-template-columns:1fr 1fr;} }
|
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||||||
|
@media(max-width:700px){ .head,.grid2{grid-template-columns:1fr;} }
|
||||||
table { width:100%; border-collapse:collapse; font-size:13px; margin-top:8px; }
|
table { width:100%; border-collapse:collapse; font-size:13px; margin-top:8px; }
|
||||||
th,td { padding:6px; border-bottom:1px solid #334155; text-align:left; vertical-align:top; }
|
th,td { padding:6px; border-bottom:1px solid #334155; text-align:left; vertical-align:top; }
|
||||||
th { color:var(--muted); font-weight:600; }
|
th { color:var(--muted); font-weight:600; }
|
||||||
@@ -36,19 +37,19 @@
|
|||||||
.msg.error { background:#7f1d1d; }
|
.msg.error { background:#7f1d1d; }
|
||||||
.msg.success { background:#064e3b; }
|
.msg.success { background:#064e3b; }
|
||||||
.actions { display:flex; gap:12px; margin-top:16px; }
|
.actions { display:flex; gap:12px; margin-top:16px; }
|
||||||
|
.badge { display:inline-block; padding:2px 10px; border-radius:99px; font-size:12px; }
|
||||||
|
.badge.dep { background:#7f1d1d; }
|
||||||
|
.badge.ven { background:#064e3b; }
|
||||||
|
.hidden { display:none; }
|
||||||
|
.hint { color:var(--muted); font-size:12px; margin-top:6px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Saisie d'un paiement</h1>
|
<h1>Saisie d'une opération</h1>
|
||||||
<div class="sub">Éclate une opération bancaire (ex. un débit Amazon groupé) en
|
<div class="sub">Montant <b>négatif</b> = dépense · <b>positif</b> = vente/encaissement.
|
||||||
plusieurs dépenses. · <a href="/dashboard/">← Tableau de bord</a> ·
|
· <a href="/dashboard/">← Tableau de bord</a></div>
|
||||||
<a href="/admin/compta/depense/">Voir les dépenses</a></div>
|
|
||||||
|
|
||||||
{% if messages %}
|
{% if messages %}{% for m in messages %}<div class="msg {{ m.tags }}">{{ m }}</div>{% endfor %}{% endif %}
|
||||||
{% for m in messages %}
|
|
||||||
<div class="msg {{ m.tags }}">{{ m }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form method="post" id="frm">
|
<form method="post" id="frm">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -66,45 +67,85 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Libellé bancaire</label>
|
<label>Libellé bancaire</label>
|
||||||
<input type="text" name="libelle" placeholder="ex. AMAZON EU SARL" required>
|
<input type="text" name="libelle" placeholder="ex. AMAZON / ALTISSIMO" required>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Montant total payé (€)</label>
|
<label>Montant signé (€) <span id="badge"></span></label>
|
||||||
<input type="number" step="0.01" name="montant_total" id="total"
|
<input type="number" step="0.01" name="montant_total" id="total"
|
||||||
placeholder="150.00" oninput="recalc()">
|
placeholder="-150 ou 168.40" oninput="toggle()">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<!-- ===== PANEL DÉPENSE (montant négatif) ===== -->
|
||||||
|
<div class="card hidden" id="panel-depense">
|
||||||
|
<div class="sub" style="margin:0 0 8px">Dépense — éclate en une ou plusieurs lignes.</div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:22%">Libellé / vendeur</th>
|
<th style="width:22%">Libellé / vendeur</th>
|
||||||
<th style="width:16%">Catégorie</th>
|
<th style="width:16%">Catégorie</th>
|
||||||
<th style="width:15%">Activité</th>
|
<th style="width:14%">Activité</th>
|
||||||
<th style="width:15%">Régime TVA</th>
|
<th style="width:16%">Régime TVA</th>
|
||||||
<th style="width:7%">Taux %</th>
|
<th style="width:7%">Taux %</th>
|
||||||
<th style="width:11%">Montant €</th>
|
<th style="width:11%">Montant €</th>
|
||||||
<th style="width:11%">TVA déduct. €</th>
|
<th style="width:11%">TVA déduct.</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="lignes"></tbody>
|
<tbody id="lignes"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="actions"><button type="button" onclick="addRow()">+ Ajouter une ligne</button></div>
|
||||||
<div class="actions">
|
|
||||||
<button type="button" onclick="addRow()">+ Ajouter une ligne</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="totaux">
|
<div class="totaux">
|
||||||
<span>Réparti : <b id="repartis">0.00</b> €</span>
|
<span>Réparti : <b id="repartis">0.00</b> €</span>
|
||||||
<span>Reste : <b id="reste" class="bad">0.00</b> €</span>
|
<span>Reste : <b id="reste" class="bad">0.00</b> €</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== PANEL VENTE (montant positif) ===== -->
|
||||||
|
<div class="card hidden" id="panel-vente">
|
||||||
|
<div class="sub" style="margin:0 0 8px">Vente — crée une facture encaissée (ou un mouvement neutre).</div>
|
||||||
|
<div class="grid2">
|
||||||
|
<div>
|
||||||
|
<label>Client existant</label>
|
||||||
|
<select name="v_client">
|
||||||
|
<option value="">—</option>
|
||||||
|
{% for c in clients %}<option value="{{ c.pk }}">{{ c.nom }} ({{ c.entreprise.nom }})</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>…ou nouveau client</label>
|
||||||
|
<input type="text" name="v_client_nouveau" placeholder="Nom du nouveau client">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Activité</label>
|
||||||
|
<select name="v_activite">
|
||||||
|
{% for val,lab in activites %}<option value="{{ val }}">{{ lab }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Taux TVA (%)</label>
|
||||||
|
<input type="number" step="0.01" name="v_taux" value="20">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Désignation</label>
|
||||||
|
<input type="text" name="v_designation" placeholder="ex. Vente de produits finis">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Ou catégorie neutre (ex. remboursement TVA)</label>
|
||||||
|
<select name="v_categorie">
|
||||||
|
<option value="">— (vente normale)</option>
|
||||||
|
{% for c in categories_vente %}<option value="{{ c.pk }}">{{ c.nom }}{% if c.exclure_calculs %} (neutre){% endif %}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hint">Le montant TTC encaissé = le montant signé saisi plus haut. HT et TVA
|
||||||
|
sont calculés depuis le taux. Si tu choisis une catégorie <b>neutre</b>, aucune facture
|
||||||
|
n'est créée (simple mouvement de trésorerie).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" class="primary">Enregistrer le paiement</button>
|
<button type="submit" class="primary">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -114,7 +155,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<select name="l_categorie">
|
<select name="l_categorie">
|
||||||
<option value="">—</option>
|
<option value="">—</option>
|
||||||
{% for c in categories %}<option value="{{ c.pk }}">{{ c.nom }}</option>{% endfor %}
|
{% for c in categories_depense %}<option value="{{ c.pk }}">{{ c.nom }}</option>{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -137,20 +178,32 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function addRow(){
|
function addRow(){
|
||||||
const tpl = document.getElementById('rowtpl');
|
document.getElementById('lignes').appendChild(
|
||||||
document.getElementById('lignes').appendChild(tpl.content.cloneNode(true));
|
document.getElementById('rowtpl').content.cloneNode(true));
|
||||||
}
|
}
|
||||||
function recalc(){
|
function recalc(){
|
||||||
let s = 0;
|
let s = 0;
|
||||||
document.querySelectorAll('.montant').forEach(i => s += parseFloat(i.value||0));
|
document.querySelectorAll('.montant').forEach(i => s += parseFloat(i.value||0));
|
||||||
const total = parseFloat(document.getElementById('total').value||0);
|
const total = Math.abs(parseFloat(document.getElementById('total').value||0));
|
||||||
document.getElementById('repartis').textContent = s.toFixed(2);
|
document.getElementById('repartis').textContent = s.toFixed(2);
|
||||||
const reste = (total - s);
|
const reste = (total - s);
|
||||||
const el = document.getElementById('reste');
|
const el = document.getElementById('reste');
|
||||||
el.textContent = reste.toFixed(2);
|
el.textContent = reste.toFixed(2);
|
||||||
el.className = Math.abs(reste) < 0.01 ? 'ok' : 'bad';
|
el.className = Math.abs(reste) < 0.01 ? 'ok' : 'bad';
|
||||||
}
|
}
|
||||||
addRow(); addRow(); addRow();
|
function toggle(){
|
||||||
|
const v = parseFloat(document.getElementById('total').value||0);
|
||||||
|
const dep = document.getElementById('panel-depense');
|
||||||
|
const ven = document.getElementById('panel-vente');
|
||||||
|
const badge = document.getElementById('badge');
|
||||||
|
if (v < 0){ dep.classList.remove('hidden'); ven.classList.add('hidden');
|
||||||
|
badge.className='badge dep'; badge.textContent='Dépense';
|
||||||
|
if(!document.querySelector('#lignes tr')){addRow();addRow();}
|
||||||
|
recalc(); }
|
||||||
|
else if (v > 0){ ven.classList.remove('hidden'); dep.classList.add('hidden');
|
||||||
|
badge.className='badge ven'; badge.textContent='Vente'; }
|
||||||
|
else { dep.classList.add('hidden'); ven.classList.add('hidden'); badge.textContent=''; }
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from . import views_saisie
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("dashboard/", views.dashboard, name="dashboard"),
|
path("dashboard/", views.dashboard, name="dashboard"),
|
||||||
|
path("dashboard/mensuel/", views.vue_mensuelle, name="vue_mensuelle"),
|
||||||
|
path("declaration/urssaf/", views.declaration_urssaf, name="declaration_urssaf"),
|
||||||
|
path("declaration/tva/", views.declaration_tva, name="declaration_tva"),
|
||||||
path("saisie/", views_saisie.saisie_paiement, name="saisie_paiement"),
|
path("saisie/", views_saisie.saisie_paiement, name="saisie_paiement"),
|
||||||
path("export/ventes/", views.export_ventes, name="export_ventes"),
|
path("export/ventes/", views.export_ventes, name="export_ventes"),
|
||||||
path("export/tva/", views.export_tva, name="export_tva"),
|
path("export/tva/", views.export_tva, name="export_tva"),
|
||||||
|
|||||||
+82
-28
@@ -8,26 +8,40 @@ from django.shortcuts import render, get_object_or_404
|
|||||||
from .models import Entreprise
|
from .models import Entreprise
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
|
MOIS = services.MOIS_FR
|
||||||
|
|
||||||
def _entreprise_courante(request):
|
|
||||||
|
def _entreprise(request):
|
||||||
eid = request.GET.get("entreprise")
|
eid = request.GET.get("entreprise")
|
||||||
qs = Entreprise.objects.all()
|
|
||||||
if eid:
|
if eid:
|
||||||
return get_object_or_404(Entreprise, pk=eid)
|
return get_object_or_404(Entreprise, pk=eid)
|
||||||
return qs.first()
|
return Entreprise.objects.first()
|
||||||
|
|
||||||
|
|
||||||
|
def _annee(request):
|
||||||
|
return int(request.GET.get("annee", date.today().year))
|
||||||
|
|
||||||
|
|
||||||
|
def _base_ctx(entreprise, annee, vue):
|
||||||
|
return {
|
||||||
|
"entreprise": entreprise,
|
||||||
|
"entreprises": Entreprise.objects.all(),
|
||||||
|
"annee": annee,
|
||||||
|
"annees": services.annees_disponibles(entreprise) or [annee],
|
||||||
|
"vue": vue,
|
||||||
|
"mois_liste": list(enumerate(MOIS, start=1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
entreprise = _entreprise_courante(request)
|
entreprise = _entreprise(request)
|
||||||
if entreprise is None:
|
if entreprise is None:
|
||||||
return render(request, "compta/dashboard.html", {"entreprise": None})
|
return render(request, "compta/dashboard.html", {"entreprise": None})
|
||||||
|
annee = _annee(request)
|
||||||
annee = int(request.GET.get("annee", date.today().year))
|
|
||||||
ctx = services.synthese(entreprise, annee)
|
ctx = services.synthese(entreprise, annee)
|
||||||
|
ctx.update(_base_ctx(entreprise, annee, "global"))
|
||||||
ctx.update({
|
ctx.update({
|
||||||
"entreprises": Entreprise.objects.all(),
|
|
||||||
"annees": services.annees_disponibles(entreprise) or [annee],
|
|
||||||
"mois_labels": ["Jan", "Fév", "Mar", "Avr", "Mai", "Juin",
|
"mois_labels": ["Jan", "Fév", "Mar", "Avr", "Mai", "Juin",
|
||||||
"Juil", "Août", "Sep", "Oct", "Nov", "Déc"],
|
"Juil", "Août", "Sep", "Oct", "Nov", "Déc"],
|
||||||
"ca_mensuel_json": [float(v) for v in ctx["ca_mensuel"]],
|
"ca_mensuel_json": [float(v) for v in ctx["ca_mensuel"]],
|
||||||
@@ -35,18 +49,59 @@ def dashboard(request):
|
|||||||
return render(request, "compta/dashboard.html", ctx)
|
return render(request, "compta/dashboard.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
def _csv_response(filename):
|
@login_required
|
||||||
|
def vue_mensuelle(request):
|
||||||
|
entreprise = _entreprise(request)
|
||||||
|
if entreprise is None:
|
||||||
|
return render(request, "compta/mensuel.html", {"entreprise": None})
|
||||||
|
annee = _annee(request)
|
||||||
|
ctx = _base_ctx(entreprise, annee, "mensuel")
|
||||||
|
ctx["rows"] = services.tableau_mensuel(entreprise, annee)
|
||||||
|
return render(request, "compta/mensuel.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def declaration_urssaf(request):
|
||||||
|
entreprise = _entreprise(request)
|
||||||
|
if entreprise is None:
|
||||||
|
return render(request, "compta/decl_urssaf.html", {"entreprise": None})
|
||||||
|
annee = _annee(request)
|
||||||
|
mois = int(request.GET.get("mois", date.today().month))
|
||||||
|
ctx = _base_ctx(entreprise, annee, "urssaf")
|
||||||
|
ctx["mois_sel"] = mois
|
||||||
|
ctx["decl"] = services.declaration_urssaf(entreprise, annee, mois)
|
||||||
|
return render(request, "compta/decl_urssaf.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def declaration_tva(request):
|
||||||
|
entreprise = _entreprise(request)
|
||||||
|
if entreprise is None:
|
||||||
|
return render(request, "compta/decl_tva.html", {"entreprise": None})
|
||||||
|
annee = _annee(request)
|
||||||
|
mois = int(request.GET.get("mois", date.today().month))
|
||||||
|
mode = request.GET.get("mode") or entreprise.vue_tva
|
||||||
|
ctx = _base_ctx(entreprise, annee, "tva")
|
||||||
|
ctx["mois_sel"] = mois
|
||||||
|
ctx["mode"] = mode
|
||||||
|
ctx["decl"] = services.declaration_tva(entreprise, annee, mois)
|
||||||
|
return render(request, "compta/decl_tva.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Exports CSV --------------------------------------------------------
|
||||||
|
|
||||||
|
def _csv(filename):
|
||||||
resp = HttpResponse(content_type="text/csv; charset=utf-8")
|
resp = HttpResponse(content_type="text/csv; charset=utf-8")
|
||||||
resp["Content-Disposition"] = f'attachment; filename="{filename}"'
|
resp["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||||
resp.write("") # BOM pour Excel
|
resp.write("")
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def export_ventes(request):
|
def export_ventes(request):
|
||||||
entreprise = _entreprise_courante(request)
|
entreprise = _entreprise(request)
|
||||||
annee = int(request.GET.get("annee", date.today().year))
|
annee = _annee(request)
|
||||||
resp = _csv_response(f"ventes_{annee}.csv")
|
resp = _csv(f"ventes_{annee}.csv")
|
||||||
w = csv.writer(resp, delimiter=";")
|
w = csv.writer(resp, delimiter=";")
|
||||||
w.writerow(["Numero", "Client", "Activite", "Date emission",
|
w.writerow(["Numero", "Client", "Activite", "Date emission",
|
||||||
"Date encaissement", "Statut", "HT", "TVA", "TTC"])
|
"Date encaissement", "Statut", "HT", "TVA", "TTC"])
|
||||||
@@ -59,13 +114,13 @@ def export_ventes(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def export_tva(request):
|
def export_tva(request):
|
||||||
entreprise = _entreprise_courante(request)
|
entreprise = _entreprise(request)
|
||||||
annee = int(request.GET.get("annee", date.today().year))
|
annee = _annee(request)
|
||||||
s = services.synthese(entreprise, annee)
|
s = services.synthese(entreprise, annee)
|
||||||
resp = _csv_response(f"tva_{annee}.csv")
|
resp = _csv(f"tva_{annee}.csv")
|
||||||
w = csv.writer(resp, delimiter=";")
|
w = csv.writer(resp, delimiter=";")
|
||||||
w.writerow(["Annee", "TVA sur ventes", "TVA autoliquidee (UE/import)",
|
w.writerow(["Annee", "TVA sur ventes", "TVA autoliquidee", "TVA collectee",
|
||||||
"TVA collectee totale", "TVA deductible", "TVA a reverser"])
|
"TVA deductible", "TVA a reverser"])
|
||||||
w.writerow([annee, s["tva_sur_ventes"], s["tva_autoliquidee"],
|
w.writerow([annee, s["tva_sur_ventes"], s["tva_autoliquidee"],
|
||||||
s["tva_collectee"], s["tva_deductible"], s["tva_a_reverser"]])
|
s["tva_collectee"], s["tva_deductible"], s["tva_a_reverser"]])
|
||||||
return resp
|
return resp
|
||||||
@@ -73,18 +128,17 @@ def export_tva(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def export_urssaf(request):
|
def export_urssaf(request):
|
||||||
entreprise = _entreprise_courante(request)
|
entreprise = _entreprise(request)
|
||||||
annee = int(request.GET.get("annee", date.today().year))
|
annee = _annee(request)
|
||||||
s = services.synthese(entreprise, annee)
|
s = services.synthese(entreprise, annee)
|
||||||
resp = _csv_response(f"urssaf_{annee}.csv")
|
resp = _csv(f"urssaf_{annee}.csv")
|
||||||
w = csv.writer(resp, delimiter=";")
|
w = csv.writer(resp, delimiter=";")
|
||||||
w.writerow(["Annee", "Activite", "CA HT", "Taux URSSAF", "Cotisations",
|
w.writerow(["Annee", "Activite", "CA HT", "Taux URSSAF", "Cotisations",
|
||||||
"Taux VL", "Versement liberatoire", "Abattement",
|
"Taux VL", "Versement liberatoire", "Abattement", "Revenu imposable"])
|
||||||
"Revenu imposable"])
|
|
||||||
for l in s["lignes_activite"]:
|
for l in s["lignes_activite"]:
|
||||||
w.writerow([annee, l["label"], l["ca_ht"], l["taux_urssaf"],
|
w.writerow([annee, l["label"], l["ca_ht"], l["taux_urssaf"], l["cotisations"],
|
||||||
l["cotisations"], l["taux_vl"], l["versement_liberatoire"],
|
l["taux_vl"], l["versement_liberatoire"], l["abattement_fiscal"],
|
||||||
l["abattement_fiscal"], l["revenu_imposable"]])
|
l["revenu_imposable"]])
|
||||||
w.writerow([annee, "TOTAL", s["ca_ht"], "", s["cotisations_urssaf"],
|
w.writerow([annee, "TOTAL", s["ca_ht"], "", s["cotisations_urssaf"], "",
|
||||||
"", s["versement_liberatoire"], "", s["revenu_imposable"]])
|
s["versement_liberatoire"], "", s["revenu_imposable"]])
|
||||||
return resp
|
return resp
|
||||||
|
|||||||
+109
-39
@@ -1,5 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Page de saisie / import : éclater un paiement bancaire en plusieurs dépenses.
|
Saisie unifiée : une opération bancaire signée.
|
||||||
|
- Montant négatif -> dépense(s) (éclatement possible).
|
||||||
|
- Montant positif -> facture (vente) avec création de client à la volée,
|
||||||
|
OU mouvement neutre (ex. remboursement de TVA).
|
||||||
"""
|
"""
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@@ -8,7 +11,10 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
|
|
||||||
from .models import Entreprise, Paiement, Depense, Activite, RegimeTva, Categorie
|
from .models import (
|
||||||
|
Entreprise, Client, Facture, LigneFacture, Paiement, Depense,
|
||||||
|
Activite, RegimeTva, Categorie,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _dec(val, defaut="0"):
|
def _dec(val, defaut="0"):
|
||||||
@@ -18,16 +24,104 @@ def _dec(val, defaut="0"):
|
|||||||
return Decimal(defaut)
|
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.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def saisie_paiement(request):
|
def saisie_paiement(request):
|
||||||
entreprises = Entreprise.objects.all()
|
entreprises = Entreprise.objects.all()
|
||||||
|
if request.method != "POST":
|
||||||
|
return render(request, "compta/saisie_paiement.html", _context(entreprises))
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first()
|
entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first()
|
||||||
d = request.POST.get("date") or date.today().isoformat()
|
d = request.POST.get("date") or date.today().isoformat()
|
||||||
libelle = (request.POST.get("libelle") or "").strip()
|
libelle = (request.POST.get("libelle") or "").strip()
|
||||||
montant_total = _dec(request.POST.get("montant_total"))
|
montant_total = _dec(request.POST.get("montant_total"))
|
||||||
|
annee = int(d[:4])
|
||||||
|
|
||||||
|
erreurs = []
|
||||||
|
if not entreprise:
|
||||||
|
erreurs.append("Sélectionne une entreprise.")
|
||||||
|
if not libelle:
|
||||||
|
erreurs.append("Renseigne le libellé du paiement.")
|
||||||
|
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 (montant positif) -> facture (vente) ou mouvement neutre
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
if montant_total > 0:
|
||||||
|
cat = cats.get(request.POST.get("v_categorie"))
|
||||||
|
if cat and cat.exclure_calculs:
|
||||||
|
# Mouvement neutre (ex. remboursement de TVA) : pas de CA.
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Vente -> facture encaissée
|
||||||
|
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 (montant négatif) -> dépense(s), éclatement possible
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
total_abs = -montant_total
|
||||||
libelles = request.POST.getlist("l_libelle")
|
libelles = request.POST.getlist("l_libelle")
|
||||||
categories = request.POST.getlist("l_categorie")
|
categories = request.POST.getlist("l_categorie")
|
||||||
activites = request.POST.getlist("l_activite")
|
activites = request.POST.getlist("l_activite")
|
||||||
@@ -44,7 +138,7 @@ def saisie_paiement(request):
|
|||||||
continue
|
continue
|
||||||
lignes.append({
|
lignes.append({
|
||||||
"libelle": lib or "(sans libellé)",
|
"libelle": lib or "(sans libellé)",
|
||||||
"categorie_id": categories[i] if i < len(categories) else "",
|
"categorie": cats.get(categories[i] if i < len(categories) else ""),
|
||||||
"activite": activites[i] if i < len(activites) else "",
|
"activite": activites[i] if i < len(activites) else "",
|
||||||
"regime": regimes[i] if i < len(regimes) else RegimeTva.NATIONALE,
|
"regime": regimes[i] if i < len(regimes) else RegimeTva.NATIONALE,
|
||||||
"taux": _dec(taux[i] if i < len(taux) else "0"),
|
"taux": _dec(taux[i] if i < len(taux) else "0"),
|
||||||
@@ -53,50 +147,26 @@ def saisie_paiement(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
somme = sum((l["montant"] for l in lignes), Decimal("0"))
|
somme = sum((l["montant"] for l in lignes), Decimal("0"))
|
||||||
erreurs = []
|
|
||||||
if not entreprise:
|
|
||||||
erreurs.append("Sélectionne une entreprise.")
|
|
||||||
if not libelle:
|
|
||||||
erreurs.append("Renseigne le libellé du paiement.")
|
|
||||||
if not lignes:
|
if not lignes:
|
||||||
erreurs.append("Ajoute au moins une ligne de dépense.")
|
messages.error(request, "Ajoute au moins une ligne de dépense.")
|
||||||
if lignes and abs(somme - montant_total) >= Decimal("0.01"):
|
return render(request, "compta/saisie_paiement.html",
|
||||||
erreurs.append(
|
_context(entreprises, request.POST))
|
||||||
f"La somme des lignes ({somme} €) ne correspond pas au "
|
if abs(somme - total_abs) >= Decimal("0.01"):
|
||||||
f"montant total ({montant_total} €). Écart : {somme - montant_total} €.")
|
messages.error(request,
|
||||||
|
f"La somme des lignes ({somme} €) ne correspond pas au montant "
|
||||||
if erreurs:
|
f"({total_abs} €). Écart : {somme - total_abs} €.")
|
||||||
for e in erreurs:
|
|
||||||
messages.error(request, e)
|
|
||||||
return render(request, "compta/saisie_paiement.html",
|
return render(request, "compta/saisie_paiement.html",
|
||||||
_context(entreprises, request.POST))
|
_context(entreprises, request.POST))
|
||||||
|
|
||||||
cats = {str(c.pk): c for c in Categorie.objects.all()}
|
|
||||||
paiement = Paiement.objects.create(
|
paiement = Paiement.objects.create(
|
||||||
entreprise=entreprise, date=d, libelle=libelle,
|
entreprise=entreprise, date=d, libelle=libelle,
|
||||||
montant_total=montant_total, sens="debit")
|
montant_total=total_abs, sens="debit")
|
||||||
for l in lignes:
|
for l in lignes:
|
||||||
Depense.objects.create(
|
Depense.objects.create(
|
||||||
entreprise=entreprise, paiement=paiement, date=d,
|
entreprise=entreprise, paiement=paiement, date=d,
|
||||||
libelle=l["libelle"], categorie=cats.get(l["categorie_id"]),
|
libelle=l["libelle"], categorie=l["categorie"],
|
||||||
activite=l["activite"] or "", regime_tva=l["regime"],
|
activite=l["activite"] or "", regime_tva=l["regime"],
|
||||||
taux_tva=l["taux"], montant_ttc=l["montant"], montant_tva=l["tva"])
|
taux_tva=l["taux"], montant_ttc=l["montant"], montant_tva=l["tva"])
|
||||||
|
messages.success(request,
|
||||||
messages.success(
|
f"Paiement « {libelle} » enregistré ({len(lignes)} dépense(s)).")
|
||||||
request,
|
|
||||||
f"Paiement « {libelle} » enregistré et éclaté en {len(lignes)} dépense(s).")
|
|
||||||
return redirect("saisie_paiement")
|
return redirect("saisie_paiement")
|
||||||
|
|
||||||
return render(request, "compta/saisie_paiement.html", _context(entreprises))
|
|
||||||
|
|
||||||
|
|
||||||
def _context(entreprises, post=None):
|
|
||||||
return {
|
|
||||||
"entreprises": entreprises,
|
|
||||||
"activites": Activite.choices,
|
|
||||||
"regimes": RegimeTva.choices,
|
|
||||||
"categories": Categorie.objects.filter(
|
|
||||||
actif=True).exclude(usage="vente"),
|
|
||||||
"today": date.today().isoformat(),
|
|
||||||
"post": post,
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user