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.
|
||||
|
||||
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
|
||||
|
||||
@@ -25,24 +21,14 @@ class StatutFacture(models.TextChoices):
|
||||
|
||||
|
||||
class RegimeTva(models.TextChoices):
|
||||
"""Régime de TVA d'une dépense (achat)."""
|
||||
NATIONALE = "nationale", "Nationale (TVA française facturée)"
|
||||
INTRACOM = "intracom", "Intracommunautaire (autoliquidation)"
|
||||
IMPORT = "import", "Import hors UE (autoliquidation)"
|
||||
|
||||
|
||||
class Categorie(models.Model):
|
||||
"""Catégorie globale (partagée entre entreprises).
|
||||
|
||||
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"),
|
||||
]
|
||||
"""Catégorie globale. exclure_calculs=True => mouvement neutre (hors fiscal)."""
|
||||
USAGE = [("depense", "Dépense"), ("vente", "Vente"), ("neutre", "Mouvement neutre")]
|
||||
nom = models.CharField(max_length=100, unique=True)
|
||||
usage = models.CharField(max_length=10, choices=USAGE, default="depense")
|
||||
exclure_calculs = models.BooleanField(
|
||||
@@ -62,21 +48,14 @@ class Categorie(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)
|
||||
annee = models.PositiveIntegerField()
|
||||
taux_urssaf = models.DecimalField(
|
||||
"Taux cotisations URSSAF (%)", max_digits=5, decimal_places=2)
|
||||
abattement_fiscal = models.DecimalField(
|
||||
"Abattement forfaitaire (%)", max_digits=5, decimal_places=2)
|
||||
taux_vl = models.DecimalField(
|
||||
"Taux versement libératoire (%)", max_digits=4, decimal_places=2)
|
||||
seuil_ca = models.DecimalField(
|
||||
"Plafond CA micro (€)", max_digits=12, decimal_places=2)
|
||||
seuil_tva_base = models.DecimalField(
|
||||
"Seuil franchise TVA (€)", max_digits=12, decimal_places=2)
|
||||
seuil_tva_majore = models.DecimalField(
|
||||
"Seuil franchise TVA majoré (€)", max_digits=12, decimal_places=2)
|
||||
taux_urssaf = models.DecimalField("Taux cotisations URSSAF (%)", max_digits=5, decimal_places=2)
|
||||
abattement_fiscal = models.DecimalField("Abattement forfaitaire (%)", max_digits=5, decimal_places=2)
|
||||
taux_vl = models.DecimalField("Taux versement libératoire (%)", max_digits=4, decimal_places=2)
|
||||
seuil_ca = models.DecimalField("Plafond CA micro (€)", max_digits=12, decimal_places=2)
|
||||
seuil_tva_base = models.DecimalField("Seuil franchise TVA (€)", max_digits=12, decimal_places=2)
|
||||
seuil_tva_majore = models.DecimalField("Seuil franchise TVA majoré (€)", max_digits=12, decimal_places=2)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Barème"
|
||||
@@ -95,19 +74,14 @@ class Entreprise(models.Model):
|
||||
activite = models.CharField(
|
||||
"Activité principale", max_length=20, choices=Activite.choices,
|
||||
default=Activite.SERVICE_BNC,
|
||||
help_text="Activité par défaut des nouvelles factures. "
|
||||
"Une entreprise peut avoir plusieurs activités : "
|
||||
"chaque facture/dépense porte la sienne.")
|
||||
help_text="Activité par défaut des nouvelles factures.")
|
||||
franchise_tva = models.BooleanField(
|
||||
"En franchise de TVA", default=False,
|
||||
help_text="Décochez si vous facturez et déclarez la TVA.")
|
||||
taux_tva_defaut = models.DecimalField(
|
||||
"Taux de TVA par défaut (%)", max_digits=4, decimal_places=2,
|
||||
default=Decimal("20.00"))
|
||||
versement_liberatoire = models.BooleanField(
|
||||
"Option versement libératoire", default=False)
|
||||
num_tva_intracom = models.CharField(
|
||||
"N° TVA intracommunautaire", max_length=20, blank=True)
|
||||
"Taux de TVA par défaut (%)", max_digits=4, decimal_places=2, default=Decimal("20.00"))
|
||||
versement_liberatoire = models.BooleanField("Option versement libératoire", default=False)
|
||||
num_tva_intracom = models.CharField("N° TVA intracommunautaire", max_length=20, blank=True)
|
||||
vue_tva = models.CharField(
|
||||
"Mode d'affichage de la déclaration TVA", max_length=10,
|
||||
choices=VUE_TVA, default="simplifie")
|
||||
@@ -128,8 +102,7 @@ class Entreprise(models.Model):
|
||||
|
||||
|
||||
class Client(models.Model):
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
||||
related_name="clients")
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="clients")
|
||||
nom = models.CharField(max_length=200)
|
||||
email = models.EmailField(blank=True)
|
||||
siret = models.CharField(max_length=20, blank=True)
|
||||
@@ -146,25 +119,18 @@ class Client(models.Model):
|
||||
|
||||
|
||||
class Facture(models.Model):
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
||||
related_name="factures")
|
||||
client = models.ForeignKey(Client, on_delete=models.PROTECT,
|
||||
related_name="factures")
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="factures")
|
||||
client = models.ForeignKey(Client, on_delete=models.PROTECT, related_name="factures")
|
||||
numero = models.CharField(max_length=50)
|
||||
activite = models.CharField(max_length=20, choices=Activite.choices)
|
||||
date_emission = models.DateField()
|
||||
date_encaissement = models.DateField(
|
||||
null=True, blank=True,
|
||||
help_text="À renseigner une fois la facture payée. "
|
||||
"Le CA micro est compté à l'encaissement.")
|
||||
statut = models.CharField(max_length=12, choices=StatutFacture.choices,
|
||||
default=StatutFacture.BROUILLON)
|
||||
montant_ht = models.DecimalField(max_digits=12, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
montant_tva = models.DecimalField(max_digits=12, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
montant_ttc = models.DecimalField(max_digits=12, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
help_text="Le CA micro est compté à l'encaissement.")
|
||||
statut = models.CharField(max_length=12, choices=StatutFacture.choices, default=StatutFacture.BROUILLON)
|
||||
montant_ht = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
|
||||
montant_tva = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
|
||||
montant_ttc = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0"))
|
||||
notes = models.TextField(blank=True)
|
||||
cree_le = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -178,7 +144,6 @@ class Facture(models.Model):
|
||||
return f"{self.numero} — {self.client}"
|
||||
|
||||
def recompute_totaux(self, save=True):
|
||||
"""Recalcule HT/TVA/TTC à partir des lignes."""
|
||||
ht = Decimal("0")
|
||||
tva = Decimal("0")
|
||||
for ligne in self.lignes.all():
|
||||
@@ -193,14 +158,11 @@ class Facture(models.Model):
|
||||
|
||||
|
||||
class LigneFacture(models.Model):
|
||||
facture = models.ForeignKey(Facture, on_delete=models.CASCADE,
|
||||
related_name="lignes")
|
||||
facture = models.ForeignKey(Facture, on_delete=models.CASCADE, related_name="lignes")
|
||||
designation = models.CharField(max_length=255)
|
||||
quantite = models.DecimalField(max_digits=12, decimal_places=3,
|
||||
default=Decimal("1"))
|
||||
quantite = models.DecimalField(max_digits=12, decimal_places=3, default=Decimal("1"))
|
||||
prix_unitaire_ht = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
taux_tva = models.DecimalField(max_digits=4, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
taux_tva = models.DecimalField(max_digits=4, decimal_places=2, default=Decimal("0"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Ligne de facture"
|
||||
@@ -215,15 +177,17 @@ class LigneFacture(models.Model):
|
||||
|
||||
|
||||
class Paiement(models.Model):
|
||||
"""Opération bancaire réelle, à éclater en une ou plusieurs dépenses
|
||||
(ex. un débit Amazon regroupant plusieurs vendeurs/factures)."""
|
||||
"""Opération bancaire réelle, à éclater en une ou plusieurs dépenses."""
|
||||
SENS = [("debit", "Débit (achat)"), ("credit", "Crédit (encaissement)")]
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
||||
related_name="paiements")
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="paiements")
|
||||
date = models.DateField()
|
||||
libelle = models.CharField("Libellé bancaire", max_length=255)
|
||||
montant_total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
sens = models.CharField(max_length=6, choices=SENS, default="debit")
|
||||
categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL,
|
||||
null=True, blank=True, related_name="paiements",
|
||||
help_text="Surtout pour les mouvements neutres "
|
||||
"(remboursement TVA, virement…).")
|
||||
cree_le = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
@@ -249,32 +213,27 @@ class Paiement(models.Model):
|
||||
|
||||
|
||||
class Depense(models.Model):
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
||||
related_name="depenses")
|
||||
paiement = models.ForeignKey(Paiement, on_delete=models.SET_NULL,
|
||||
null=True, blank=True, related_name="depenses",
|
||||
help_text="Opération bancaire dont cette dépense "
|
||||
"fait partie (achat groupé éclaté).")
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="depenses")
|
||||
paiement = models.ForeignKey(Paiement, on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name="depenses",
|
||||
help_text="Opération bancaire dont cette dépense fait partie.")
|
||||
date = models.DateField()
|
||||
activite = models.CharField(
|
||||
max_length=20, choices=Activite.choices, blank=True,
|
||||
help_text="Activité concernée (facultatif, pour la ventilation).")
|
||||
categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL,
|
||||
null=True, blank=True, related_name="depenses")
|
||||
categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name="depenses")
|
||||
libelle = models.CharField(max_length=255)
|
||||
montant_ttc = models.DecimalField(
|
||||
"Montant payé (€)", max_digits=12, decimal_places=2,
|
||||
help_text="Pour un achat UE/import, c'est le montant HT (sans TVA facturée).")
|
||||
montant_tva = models.DecimalField(
|
||||
"TVA déductible facturée (€)", max_digits=12, decimal_places=2,
|
||||
default=Decimal("0"),
|
||||
"TVA déductible facturée (€)", max_digits=12, decimal_places=2, default=Decimal("0"),
|
||||
help_text="TVA payée au fournisseur (achats nationaux uniquement).")
|
||||
regime_tva = models.CharField(
|
||||
"Régime de TVA", max_length=12, choices=RegimeTva.choices,
|
||||
default=RegimeTva.NATIONALE)
|
||||
"Régime de TVA", max_length=12, choices=RegimeTva.choices, default=RegimeTva.NATIONALE)
|
||||
taux_tva = models.DecimalField(
|
||||
"Taux TVA autoliquidation (%)", max_digits=4, decimal_places=2,
|
||||
default=Decimal("0"),
|
||||
"Taux TVA autoliquidation (%)", max_digits=4, decimal_places=2, default=Decimal("0"),
|
||||
help_text="Taux français à auto-liquider pour les achats UE/import (ex. 20).")
|
||||
cree_le = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -288,33 +247,26 @@ class Depense(models.Model):
|
||||
|
||||
@property
|
||||
def neutre(self):
|
||||
"""Mouvement neutre : exclu des calculs fiscaux."""
|
||||
return bool(self.categorie and self.categorie.exclure_calculs)
|
||||
|
||||
@property
|
||||
def autoliquidee(self):
|
||||
"""True si la TVA est auto-liquidée (achat UE ou import)."""
|
||||
return self.regime_tva in (RegimeTva.INTRACOM, RegimeTva.IMPORT)
|
||||
|
||||
@property
|
||||
def montant_ht(self):
|
||||
# En autoliquidation, le fournisseur ne facture pas de TVA : HT = montant payé.
|
||||
if self.autoliquidee:
|
||||
return self.montant_ttc.quantize(Decimal("0.01"))
|
||||
return (self.montant_ttc - self.montant_tva).quantize(Decimal("0.01"))
|
||||
|
||||
@property
|
||||
def tva_autoliquidee(self):
|
||||
"""TVA française auto-liquidée (comptée en collectée ET en déductible)."""
|
||||
if self.autoliquidee and not self.neutre:
|
||||
return (self.montant_ttc * self.taux_tva / Decimal("100")).quantize(
|
||||
Decimal("0.01"))
|
||||
return (self.montant_ttc * self.taux_tva / Decimal("100")).quantize(Decimal("0.01"))
|
||||
return Decimal("0.00")
|
||||
|
||||
@property
|
||||
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:
|
||||
return Decimal("0.00")
|
||||
if self.autoliquidee:
|
||||
|
||||
+185
-97
@@ -1,96 +1,110 @@
|
||||
"""
|
||||
Logique fiscale — JB Compta.
|
||||
|
||||
Bases de calcul : CA HORS TAXES (HT) encaissé.
|
||||
- Base des cotisations URSSAF et du versement libératoire (assujetti TVA).
|
||||
- En franchise de TVA, HT = TTC : le calcul reste correct.
|
||||
|
||||
Multi-activité : une entreprise peut exercer plusieurs activités (ex. Vente BIC
|
||||
+ Prestation BIC). 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.
|
||||
Bases : CA HORS TAXES (HT) encaissé.
|
||||
TVA collectée = TVA ventes + TVA auto-liquidée (UE/import).
|
||||
TVA déductible = TVA achats nationaux + TVA auto-liquidée. Neutres exclus.
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.db.models import Sum
|
||||
from django.db.models.functions import TruncMonth
|
||||
|
||||
from .models import Facture, Depense, Bareme, StatutFacture, Activite
|
||||
from .models import (Facture, Depense, LigneFacture, Bareme, StatutFacture,
|
||||
Activite, RegimeTva)
|
||||
|
||||
CENT = Decimal("0.01")
|
||||
MOIS_FR = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet",
|
||||
"Août", "Septembre", "Octobre", "Novembre", "Décembre"]
|
||||
|
||||
|
||||
def _q(value):
|
||||
return (value or Decimal("0")).quantize(CENT)
|
||||
def _q(v):
|
||||
return (v or Decimal("0")).quantize(CENT)
|
||||
|
||||
|
||||
def factures_encaissees(entreprise, annee):
|
||||
return Facture.objects.filter(
|
||||
entreprise=entreprise,
|
||||
statut=StatutFacture.ENCAISSEE,
|
||||
date_encaissement__year=annee,
|
||||
)
|
||||
# ---- Sélections de période ---------------------------------------------
|
||||
|
||||
def factures_encaissees(entreprise, annee, mois=None):
|
||||
qs = Facture.objects.filter(entreprise=entreprise, statut=StatutFacture.ENCAISSEE,
|
||||
date_encaissement__year=annee)
|
||||
if mois:
|
||||
qs = qs.filter(date_encaissement__month=mois)
|
||||
return qs
|
||||
|
||||
|
||||
def depenses_annee(entreprise, annee):
|
||||
return Depense.objects.filter(entreprise=entreprise, date__year=annee)
|
||||
def depenses_periode(entreprise, annee, mois=None):
|
||||
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):
|
||||
"""Dict {activite: CA HT encaissé}."""
|
||||
rows = (
|
||||
factures_encaissees(entreprise, annee)
|
||||
.values("activite")
|
||||
.annotate(total=Sum("montant_ht"))
|
||||
)
|
||||
# ---- CA -----------------------------------------------------------------
|
||||
|
||||
def ca_ht_par_activite(entreprise, annee, mois=None):
|
||||
rows = factures_encaissees(entreprise, annee, mois).values("activite").annotate(
|
||||
total=Sum("montant_ht"))
|
||||
return {r["activite"]: _q(r["total"]) for r in rows}
|
||||
|
||||
|
||||
def ca_ht(entreprise, annee):
|
||||
agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_ht"))
|
||||
return _q(agg["s"])
|
||||
def ca_ht(entreprise, annee, mois=None):
|
||||
return _q(factures_encaissees(entreprise, annee, mois).aggregate(
|
||||
s=Sum("montant_ht"))["s"])
|
||||
|
||||
|
||||
# ---- TVA ----------------------------------------------------------------
|
||||
|
||||
def tva_sur_ventes(entreprise, annee):
|
||||
agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_tva"))
|
||||
return _q(agg["s"])
|
||||
def tva_sur_ventes(entreprise, annee, mois=None):
|
||||
return _q(factures_encaissees(entreprise, annee, mois).aggregate(
|
||||
s=Sum("montant_tva"))["s"])
|
||||
|
||||
|
||||
def tva_autoliquidee(entreprise, annee):
|
||||
"""TVA auto-liquidée sur les achats UE/import (intracom + import)."""
|
||||
total = sum((d.tva_autoliquidee for d in depenses_annee(entreprise, annee)),
|
||||
def tva_autoliquidee(entreprise, annee, mois=None):
|
||||
total = sum((d.tva_autoliquidee for d in depenses_periode(entreprise, annee, mois)),
|
||||
Decimal("0"))
|
||||
return _q(total)
|
||||
|
||||
|
||||
def tva_deductible(entreprise, annee):
|
||||
"""TVA récupérable : facturée (national) + auto-liquidée (UE/import)."""
|
||||
total = sum((d.tva_deductible_value for d in depenses_annee(entreprise, annee)),
|
||||
def tva_deductible(entreprise, annee, mois=None):
|
||||
total = sum((d.tva_deductible_value for d in depenses_periode(entreprise, annee, mois)),
|
||||
Decimal("0"))
|
||||
return _q(total)
|
||||
|
||||
|
||||
def tva_collectee(entreprise, annee):
|
||||
"""TVA due : sur ventes + auto-liquidée sur achats UE/import."""
|
||||
return _q(tva_sur_ventes(entreprise, annee) + tva_autoliquidee(entreprise, annee))
|
||||
def tva_collectee(entreprise, annee, mois=None):
|
||||
return _q(tva_sur_ventes(entreprise, annee, mois)
|
||||
+ 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):
|
||||
"""Liste de 12 valeurs (index 0 = janvier) du CA HT encaissé (toutes activités)."""
|
||||
rows = (
|
||||
factures_encaissees(entreprise, annee)
|
||||
.annotate(mois=TruncMonth("date_encaissement"))
|
||||
.values("mois")
|
||||
.annotate(total=Sum("montant_ht"))
|
||||
)
|
||||
rows = (factures_encaissees(entreprise, annee)
|
||||
.annotate(mois=TruncMonth("date_encaissement"))
|
||||
.values("mois").annotate(total=Sum("montant_ht")))
|
||||
mensuel = [Decimal("0")] * 12
|
||||
for r in rows:
|
||||
if r["mois"]:
|
||||
@@ -101,7 +115,6 @@ def ca_ht_mensuel(entreprise, annee):
|
||||
# ---- Détail par activité ------------------------------------------------
|
||||
|
||||
def detail_activite(activite, ca, bareme):
|
||||
"""Calculs fiscaux pour une activité donnée."""
|
||||
label = dict(Activite.choices).get(activite, activite)
|
||||
d = {"activite": activite, "label": label, "ca_ht": ca, "bareme": bareme}
|
||||
if bareme:
|
||||
@@ -111,72 +124,147 @@ def detail_activite(activite, ca, bareme):
|
||||
"taux_vl": bareme.taux_vl,
|
||||
"versement_liberatoire": _q(ca * bareme.taux_vl / Decimal("100")),
|
||||
"abattement_fiscal": bareme.abattement_fiscal,
|
||||
"revenu_imposable": _q(
|
||||
ca * (Decimal("100") - bareme.abattement_fiscal) / Decimal("100")),
|
||||
"revenu_imposable": _q(ca * (Decimal("100") - bareme.abattement_fiscal) / Decimal("100")),
|
||||
"seuil_ca": bareme.seuil_ca,
|
||||
"pct_seuil_ca": (_q(ca / bareme.seuil_ca * Decimal("100"))
|
||||
if bareme.seuil_ca else Decimal("0")),
|
||||
"pct_seuil_ca": (_q(ca / bareme.seuil_ca * Decimal("100")) if bareme.seuil_ca else Decimal("0")),
|
||||
"seuil_tva_base": bareme.seuil_tva_base,
|
||||
"depasse_seuil_tva": ca > bareme.seuil_tva_base,
|
||||
"depasse_seuil_ca": ca > bareme.seuil_ca,
|
||||
})
|
||||
else:
|
||||
d.update({
|
||||
"taux_urssaf": None, "cotisations": None,
|
||||
"taux_vl": None, "versement_liberatoire": None,
|
||||
"abattement_fiscal": None, "revenu_imposable": None,
|
||||
"seuil_ca": None, "pct_seuil_ca": None,
|
||||
"seuil_tva_base": None, "depasse_seuil_tva": False,
|
||||
"depasse_seuil_ca": False,
|
||||
})
|
||||
for k in ("taux_urssaf", "cotisations", "taux_vl", "versement_liberatoire",
|
||||
"abattement_fiscal", "revenu_imposable", "seuil_ca", "pct_seuil_ca",
|
||||
"seuil_tva_base"):
|
||||
d[k] = None
|
||||
d["depasse_seuil_tva"] = d["depasse_seuil_ca"] = False
|
||||
return d
|
||||
|
||||
|
||||
def synthese(entreprise, annee):
|
||||
"""Tableau de bord chiffré, ventilé par activité puis totalisé."""
|
||||
ca_par_act = ca_ht_par_activite(entreprise, annee)
|
||||
|
||||
activites = set(ca_par_act) | {entreprise.activite}
|
||||
def _lignes_activite(entreprise, annee, mois=None):
|
||||
ca_par = ca_ht_par_activite(entreprise, annee, mois)
|
||||
activites = set(ca_par) | {entreprise.activite}
|
||||
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):
|
||||
vals = [l[cle] for l in lignes if l.get(cle) is not None]
|
||||
return _q(sum(vals, Decimal("0"))) if vals else None
|
||||
def _somme(lignes, cle):
|
||||
vals = [l[cle] for l in lignes if l.get(cle) is not None]
|
||||
return _q(sum(vals, Decimal("0"))) if vals else None
|
||||
|
||||
|
||||
# ---- Vue globale (annuelle) --------------------------------------------
|
||||
|
||||
def synthese(entreprise, annee):
|
||||
lignes = _lignes_activite(entreprise, annee)
|
||||
tva_ventes = tva_sur_ventes(entreprise, annee)
|
||||
tva_autoliq = tva_autoliquidee(entreprise, annee)
|
||||
tva_ded = tva_deductible(entreprise, annee)
|
||||
tva_col = _q(tva_ventes + tva_autoliq)
|
||||
|
||||
return {
|
||||
"entreprise": entreprise,
|
||||
"annee": annee,
|
||||
"lignes_activite": lignes,
|
||||
"entreprise": entreprise, "annee": annee, "lignes_activite": lignes,
|
||||
"bareme_manquant": any(l["bareme"] is None and l["ca_ht"] for l in lignes),
|
||||
"ca_ht": ca_ht(entreprise, annee),
|
||||
"cotisations_urssaf": somme("cotisations"),
|
||||
"versement_liberatoire": (
|
||||
somme("versement_liberatoire") if entreprise.versement_liberatoire else None),
|
||||
"revenu_imposable": somme("revenu_imposable"),
|
||||
"tva_sur_ventes": tva_ventes,
|
||||
"tva_autoliquidee": tva_autoliq,
|
||||
"tva_collectee": tva_col,
|
||||
"tva_deductible": tva_ded,
|
||||
"cotisations_urssaf": _somme(lignes, "cotisations"),
|
||||
"versement_liberatoire": (_somme(lignes, "versement_liberatoire")
|
||||
if entreprise.versement_liberatoire else None),
|
||||
"revenu_imposable": _somme(lignes, "revenu_imposable"),
|
||||
"tva_sur_ventes": tva_ventes, "tva_autoliquidee": tva_autoliq,
|
||||
"tva_collectee": tva_col, "tva_deductible": tva_ded,
|
||||
"tva_a_reverser": _q(tva_col - tva_ded),
|
||||
"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):
|
||||
qs = (
|
||||
Facture.objects.filter(entreprise=entreprise,
|
||||
statut=StatutFacture.ENCAISSEE,
|
||||
date_encaissement__isnull=False)
|
||||
.dates("date_encaissement", "year")
|
||||
)
|
||||
qs = Facture.objects.filter(entreprise=entreprise, statut=StatutFacture.ENCAISSEE,
|
||||
date_encaissement__isnull=False).dates("date_encaissement", "year")
|
||||
return sorted({d.year for d in qs}, reverse=True)
|
||||
|
||||
@@ -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>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>JB Compta — Tableau de bord</title>
|
||||
<style>
|
||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
||||
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
|
||||
background:var(--bg); color:var(--txt); padding:24px; }
|
||||
h1 { margin:0 0 4px; font-size:22px; }
|
||||
h2 { font-size:15px; color:var(--muted); margin:24px 0 10px; font-weight:600; }
|
||||
a { color:var(--accent); }
|
||||
.sub { color:var(--muted); margin-bottom:20px; }
|
||||
form.filters { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:24px; }
|
||||
select, button { background:var(--card); color:var(--txt); border:1px solid #334155;
|
||||
border-radius:8px; padding:8px 12px; font-size:14px; }
|
||||
button { cursor:pointer; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
|
||||
gap:16px; margin-bottom:8px; }
|
||||
.card { background:var(--card); border:1px solid #334155; border-radius:12px;
|
||||
padding:16px; }
|
||||
.card .label { color:var(--muted); font-size:13px; }
|
||||
.card .value { font-size:26px; font-weight:600; margin-top:6px; }
|
||||
.card .hint { color:var(--muted); font-size:12px; margin-top:4px; }
|
||||
.ok{color:var(--ok)} .warn{color:var(--warn)} .bad{color:var(--bad)}
|
||||
.bar { height:8px; background:#334155; border-radius:99px; overflow:hidden; margin-top:8px;}
|
||||
.bar > span { display:block; height:100%; background:var(--accent); }
|
||||
.row { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-top:8px; }
|
||||
@media(max-width:800px){ .row{grid-template-columns:1fr;} }
|
||||
.exports a { display:inline-block; margin-right:12px; }
|
||||
canvas { max-width:100%; }
|
||||
table { width:100%; border-collapse:collapse; font-size:14px; }
|
||||
td,th { padding:8px; border-bottom:1px solid #334155; text-align:right; }
|
||||
th:first-child, td:first-child { text-align:left; }
|
||||
tr.total td { font-weight:600; border-top:2px solid #475569; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% if not entreprise %}
|
||||
<h1>Aucune entreprise</h1>
|
||||
<p class="sub">Crée une entreprise et des factures dans
|
||||
<a href="/admin/">l'administration</a>, puis reviens ici.</p>
|
||||
{% else %}
|
||||
<h1>{{ entreprise.nom }}</h1>
|
||||
<div class="sub">{{ entreprise.get_activite_display }} (principale) ·
|
||||
{% if entreprise.franchise_tva %}Franchise de TVA{% else %}Assujetti TVA{% endif %}
|
||||
{% if entreprise.versement_liberatoire %}· Versement libératoire{% endif %}
|
||||
· Exercice {{ annee }}</div>
|
||||
|
||||
<form class="filters" method="get">
|
||||
<select name="entreprise" onchange="this.form.submit()">
|
||||
{% for e in entreprises %}
|
||||
<option value="{{ e.pk }}" {% if e.pk == entreprise.pk %}selected{% endif %}>{{ e.nom }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="annee" onchange="this.form.submit()">
|
||||
{% for a in annees %}
|
||||
<option value="{{ a }}" {% if a == annee %}selected{% endif %}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<a href="/saisie/"><button type="button">+ Saisir un paiement</button></a>
|
||||
<a href="/admin/"><button type="button">Admin</button></a>
|
||||
</form>
|
||||
{% extends "compta/base.html" %}
|
||||
{% block titre %}Global{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if bareme_manquant %}
|
||||
<div class="card bad" style="margin-bottom:16px;">
|
||||
Une activité avec du CA n'a pas de barème {{ annee }}. Ajoute-le dans
|
||||
<a href="/admin/compta/bareme/">Barèmes</a>.
|
||||
Une activité avec du CA n'a pas de barème {{ annee }}.
|
||||
Ajoute-le dans <a href="/admin/compta/bareme/">Barèmes</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Totaux entreprise -->
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">CA HT encaissé (total)</div>
|
||||
<div class="value">{{ ca_ht }} €</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Cotisations URSSAF (total)</div>
|
||||
<div class="label">Cotisations URSSAF</div>
|
||||
<div class="value">{{ cotisations_urssaf|default:"—" }} €</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -91,93 +26,55 @@
|
||||
<div class="label">TVA à reverser</div>
|
||||
<div class="value">{{ tva_a_reverser }} €</div>
|
||||
<div class="hint">{{ tva_collectee }} collectée − {{ tva_deductible }} déductible</div>
|
||||
{% 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>
|
||||
|
||||
<!-- Ventilation par activité -->
|
||||
<h2>Ventilation par activité</h2>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Activité</th><th>CA HT</th><th>% plafond</th>
|
||||
<th>URSSAF</th><th>Impôt (VL)</th><th>Revenu imposable</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<thead><tr><th>Activité</th><th>CA HT</th><th>% plafond</th><th>URSSAF</th><th>Impôt (VL)</th><th>Revenu imposable</th></tr></thead>
|
||||
<tbody>
|
||||
{% for l in lignes_activite %}
|
||||
<tr>
|
||||
<td>{{ l.label }}</td>
|
||||
<td>{{ l.ca_ht }} €</td>
|
||||
<td class="{% if l.depasse_seuil_ca %}bad{% endif %}">
|
||||
{% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="{% if l.depasse_seuil_ca %}bad{% endif %}">{% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %}</td>
|
||||
<td>{% if l.cotisations is not None %}{{ l.cotisations }} €{% else %}—{% endif %}</td>
|
||||
<td>{% if l.versement_liberatoire is not None %}{{ l.versement_liberatoire }} €{% else %}—{% endif %}</td>
|
||||
<td>{% if l.revenu_imposable is not None %}{{ l.revenu_imposable }} €{% else %}—{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total">
|
||||
<td>Total</td>
|
||||
<td>{{ ca_ht }} €</td>
|
||||
<td></td>
|
||||
<td>{{ cotisations_urssaf|default:"—" }} €</td>
|
||||
<td>{{ versement_liberatoire|default:"—" }} €</td>
|
||||
<td>{{ revenu_imposable|default:"—" }} €</td>
|
||||
</tr>
|
||||
<tr class="total"><td>Total</td><td>{{ ca_ht }} €</td><td></td><td>{{ cotisations_urssaf|default:"—" }} €</td><td>{{ versement_liberatoire|default:"—" }} €</td><td>{{ revenu_imposable|default:"—" }} €</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 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="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>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label" style="margin-bottom:8px;">À provisionner (année)</div>
|
||||
<table>
|
||||
<tr><th>Poste</th><th>Montant</th></tr>
|
||||
<tr><td>URSSAF</td><td>{{ cotisations_urssaf|default:"—" }} €</td></tr>
|
||||
<tr><td>Impôt (VL)</td><td>{{ versement_liberatoire|default:"—" }} €</td></tr>
|
||||
<tr><td>TVA</td><td>{{ tva_a_reverser }} €</td></tr>
|
||||
</table>
|
||||
<div class="exports" style="margin-top:16px;">
|
||||
<div class="label" style="margin-bottom:6px;">Exports CSV</div>
|
||||
<div class="label" style="margin-bottom:8px;">Exports CSV</div>
|
||||
<div class="exports">
|
||||
<a href="{% url 'export_ventes' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">Ventes</a>
|
||||
<a href="{% url 'export_tva' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">TVA</a>
|
||||
<a href="{% url 'export_urssaf' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">URSSAF</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
const ctx = document.getElementById('caChart');
|
||||
new Chart(ctx, {
|
||||
new Chart(document.getElementById('caChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {{ mois_labels|safe }},
|
||||
datasets: [{ label: 'CA HT (€)', data: {{ ca_mensuel_json|safe }},
|
||||
backgroundColor: '#38bdf8' }]
|
||||
},
|
||||
data: { labels: {{ mois_labels|safe }},
|
||||
datasets: [{ label:'CA HT (€)', data: {{ ca_mensuel_json|safe }}, backgroundColor:'#38bdf8' }] },
|
||||
options: { plugins:{legend:{display:false}},
|
||||
scales:{ x:{ticks:{color:'#94a3b8'}}, y:{ticks:{color:'#94a3b8'}} } }
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>JB Compta — Saisie d'un paiement</title>
|
||||
<title>JB Compta — Saisie</title>
|
||||
<style>
|
||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
||||
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
|
||||
@@ -19,7 +19,8 @@
|
||||
input, select { background:#0f172a; color:var(--txt); border:1px solid #334155;
|
||||
border-radius:8px; padding:8px 10px; font-size:14px; width:100%; }
|
||||
.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; }
|
||||
th,td { padding:6px; border-bottom:1px solid #334155; text-align:left; vertical-align:top; }
|
||||
th { color:var(--muted); font-weight:600; }
|
||||
@@ -36,19 +37,19 @@
|
||||
.msg.error { background:#7f1d1d; }
|
||||
.msg.success { background:#064e3b; }
|
||||
.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>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Saisie d'un paiement</h1>
|
||||
<div class="sub">Éclate une opération bancaire (ex. un débit Amazon groupé) en
|
||||
plusieurs dépenses. · <a href="/dashboard/">← Tableau de bord</a> ·
|
||||
<a href="/admin/compta/depense/">Voir les dépenses</a></div>
|
||||
<h1>Saisie d'une opération</h1>
|
||||
<div class="sub">Montant <b>négatif</b> = dépense · <b>positif</b> = vente/encaissement.
|
||||
· <a href="/dashboard/">← Tableau de bord</a></div>
|
||||
|
||||
{% if messages %}
|
||||
{% for m in messages %}
|
||||
<div class="msg {{ m.tags }}">{{ m }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if messages %}{% for m in messages %}<div class="msg {{ m.tags }}">{{ m }}</div>{% endfor %}{% endif %}
|
||||
|
||||
<form method="post" id="frm">
|
||||
{% csrf_token %}
|
||||
@@ -66,45 +67,85 @@
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<label>Montant total payé (€)</label>
|
||||
<label>Montant signé (€) <span id="badge"></span></label>
|
||||
<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 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>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22%">Libellé / vendeur</th>
|
||||
<th style="width:16%">Catégorie</th>
|
||||
<th style="width:15%">Activité</th>
|
||||
<th style="width:15%">Régime TVA</th>
|
||||
<th style="width:14%">Activité</th>
|
||||
<th style="width:16%">Régime TVA</th>
|
||||
<th style="width:7%">Taux %</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="lignes"></tbody>
|
||||
</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">
|
||||
<span>Réparti : <b id="repartis">0.00</b> €</span>
|
||||
<span>Reste : <b id="reste" class="bad">0.00</b> €</span>
|
||||
</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">
|
||||
<button type="submit" class="primary">Enregistrer le paiement</button>
|
||||
<button type="submit" class="primary">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -114,7 +155,7 @@
|
||||
<td>
|
||||
<select name="l_categorie">
|
||||
<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>
|
||||
</td>
|
||||
<td>
|
||||
@@ -137,20 +178,32 @@
|
||||
|
||||
<script>
|
||||
function addRow(){
|
||||
const tpl = document.getElementById('rowtpl');
|
||||
document.getElementById('lignes').appendChild(tpl.content.cloneNode(true));
|
||||
document.getElementById('lignes').appendChild(
|
||||
document.getElementById('rowtpl').content.cloneNode(true));
|
||||
}
|
||||
function recalc(){
|
||||
let s = 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);
|
||||
const reste = (total - s);
|
||||
const el = document.getElementById('reste');
|
||||
el.textContent = reste.toFixed(2);
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,6 +4,9 @@ from . import views_saisie
|
||||
|
||||
urlpatterns = [
|
||||
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("export/ventes/", views.export_ventes, name="export_ventes"),
|
||||
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 . import services
|
||||
|
||||
MOIS = services.MOIS_FR
|
||||
|
||||
def _entreprise_courante(request):
|
||||
|
||||
def _entreprise(request):
|
||||
eid = request.GET.get("entreprise")
|
||||
qs = Entreprise.objects.all()
|
||||
if 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
|
||||
def dashboard(request):
|
||||
entreprise = _entreprise_courante(request)
|
||||
entreprise = _entreprise(request)
|
||||
if entreprise is None:
|
||||
return render(request, "compta/dashboard.html", {"entreprise": None})
|
||||
|
||||
annee = int(request.GET.get("annee", date.today().year))
|
||||
annee = _annee(request)
|
||||
ctx = services.synthese(entreprise, annee)
|
||||
ctx.update(_base_ctx(entreprise, annee, "global"))
|
||||
ctx.update({
|
||||
"entreprises": Entreprise.objects.all(),
|
||||
"annees": services.annees_disponibles(entreprise) or [annee],
|
||||
"mois_labels": ["Jan", "Fév", "Mar", "Avr", "Mai", "Juin",
|
||||
"Juil", "Août", "Sep", "Oct", "Nov", "Déc"],
|
||||
"ca_mensuel_json": [float(v) for v in ctx["ca_mensuel"]],
|
||||
@@ -35,18 +49,59 @@ def dashboard(request):
|
||||
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["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
resp.write("") # BOM pour Excel
|
||||
resp.write("")
|
||||
return resp
|
||||
|
||||
|
||||
@login_required
|
||||
def export_ventes(request):
|
||||
entreprise = _entreprise_courante(request)
|
||||
annee = int(request.GET.get("annee", date.today().year))
|
||||
resp = _csv_response(f"ventes_{annee}.csv")
|
||||
entreprise = _entreprise(request)
|
||||
annee = _annee(request)
|
||||
resp = _csv(f"ventes_{annee}.csv")
|
||||
w = csv.writer(resp, delimiter=";")
|
||||
w.writerow(["Numero", "Client", "Activite", "Date emission",
|
||||
"Date encaissement", "Statut", "HT", "TVA", "TTC"])
|
||||
@@ -59,13 +114,13 @@ def export_ventes(request):
|
||||
|
||||
@login_required
|
||||
def export_tva(request):
|
||||
entreprise = _entreprise_courante(request)
|
||||
annee = int(request.GET.get("annee", date.today().year))
|
||||
entreprise = _entreprise(request)
|
||||
annee = _annee(request)
|
||||
s = services.synthese(entreprise, annee)
|
||||
resp = _csv_response(f"tva_{annee}.csv")
|
||||
resp = _csv(f"tva_{annee}.csv")
|
||||
w = csv.writer(resp, delimiter=";")
|
||||
w.writerow(["Annee", "TVA sur ventes", "TVA autoliquidee (UE/import)",
|
||||
"TVA collectee totale", "TVA deductible", "TVA a reverser"])
|
||||
w.writerow(["Annee", "TVA sur ventes", "TVA autoliquidee", "TVA collectee",
|
||||
"TVA deductible", "TVA a reverser"])
|
||||
w.writerow([annee, s["tva_sur_ventes"], s["tva_autoliquidee"],
|
||||
s["tva_collectee"], s["tva_deductible"], s["tva_a_reverser"]])
|
||||
return resp
|
||||
@@ -73,18 +128,17 @@ def export_tva(request):
|
||||
|
||||
@login_required
|
||||
def export_urssaf(request):
|
||||
entreprise = _entreprise_courante(request)
|
||||
annee = int(request.GET.get("annee", date.today().year))
|
||||
entreprise = _entreprise(request)
|
||||
annee = _annee(request)
|
||||
s = services.synthese(entreprise, annee)
|
||||
resp = _csv_response(f"urssaf_{annee}.csv")
|
||||
resp = _csv(f"urssaf_{annee}.csv")
|
||||
w = csv.writer(resp, delimiter=";")
|
||||
w.writerow(["Annee", "Activite", "CA HT", "Taux URSSAF", "Cotisations",
|
||||
"Taux VL", "Versement liberatoire", "Abattement",
|
||||
"Revenu imposable"])
|
||||
"Taux VL", "Versement liberatoire", "Abattement", "Revenu imposable"])
|
||||
for l in s["lignes_activite"]:
|
||||
w.writerow([annee, l["label"], l["ca_ht"], l["taux_urssaf"],
|
||||
l["cotisations"], l["taux_vl"], l["versement_liberatoire"],
|
||||
l["abattement_fiscal"], l["revenu_imposable"]])
|
||||
w.writerow([annee, "TOTAL", s["ca_ht"], "", s["cotisations_urssaf"],
|
||||
"", s["versement_liberatoire"], "", s["revenu_imposable"]])
|
||||
w.writerow([annee, l["label"], l["ca_ht"], l["taux_urssaf"], l["cotisations"],
|
||||
l["taux_vl"], l["versement_liberatoire"], l["abattement_fiscal"],
|
||||
l["revenu_imposable"]])
|
||||
w.writerow([annee, "TOTAL", s["ca_ht"], "", s["cotisations_urssaf"], "",
|
||||
s["versement_liberatoire"], "", s["revenu_imposable"]])
|
||||
return resp
|
||||
|
||||
+144
-74
@@ -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 datetime import date
|
||||
@@ -8,7 +11,10 @@ from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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"):
|
||||
@@ -18,85 +24,149 @@ def _dec(val, defaut="0"):
|
||||
return Decimal(defaut)
|
||||
|
||||
|
||||
@login_required
|
||||
def saisie_paiement(request):
|
||||
entreprises = Entreprise.objects.all()
|
||||
|
||||
if request.method == "POST":
|
||||
entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first()
|
||||
d = request.POST.get("date") or date.today().isoformat()
|
||||
libelle = (request.POST.get("libelle") or "").strip()
|
||||
montant_total = _dec(request.POST.get("montant_total"))
|
||||
|
||||
libelles = request.POST.getlist("l_libelle")
|
||||
categories = request.POST.getlist("l_categorie")
|
||||
activites = request.POST.getlist("l_activite")
|
||||
regimes = request.POST.getlist("l_regime")
|
||||
taux = request.POST.getlist("l_taux")
|
||||
montants = request.POST.getlist("l_montant")
|
||||
tvas = request.POST.getlist("l_tva")
|
||||
|
||||
lignes = []
|
||||
for i in range(len(libelles)):
|
||||
m = _dec(montants[i] if i < len(montants) else "0")
|
||||
lib = (libelles[i] or "").strip()
|
||||
if m == 0 and not lib:
|
||||
continue
|
||||
lignes.append({
|
||||
"libelle": lib or "(sans libellé)",
|
||||
"categorie_id": categories[i] if i < len(categories) else "",
|
||||
"activite": activites[i] if i < len(activites) else "",
|
||||
"regime": regimes[i] if i < len(regimes) else RegimeTva.NATIONALE,
|
||||
"taux": _dec(taux[i] if i < len(taux) else "0"),
|
||||
"montant": m,
|
||||
"tva": _dec(tvas[i] if i < len(tvas) else "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:
|
||||
erreurs.append("Ajoute au moins une ligne de dépense.")
|
||||
if lignes and abs(somme - montant_total) >= Decimal("0.01"):
|
||||
erreurs.append(
|
||||
f"La somme des lignes ({somme} €) ne correspond pas au "
|
||||
f"montant total ({montant_total} €). Écart : {somme - montant_total} €.")
|
||||
|
||||
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()}
|
||||
paiement = Paiement.objects.create(
|
||||
entreprise=entreprise, date=d, libelle=libelle,
|
||||
montant_total=montant_total, sens="debit")
|
||||
for l in lignes:
|
||||
Depense.objects.create(
|
||||
entreprise=entreprise, paiement=paiement, date=d,
|
||||
libelle=l["libelle"], categorie=cats.get(l["categorie_id"]),
|
||||
activite=l["activite"] or "", regime_tva=l["regime"],
|
||||
taux_tva=l["taux"], montant_ttc=l["montant"], montant_tva=l["tva"])
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Paiement « {libelle} » enregistré et éclaté en {len(lignes)} dépense(s).")
|
||||
return redirect("saisie_paiement")
|
||||
|
||||
return render(request, "compta/saisie_paiement.html", _context(entreprises))
|
||||
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": Categorie.objects.filter(
|
||||
actif=True).exclude(usage="vente"),
|
||||
"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
|
||||
def saisie_paiement(request):
|
||||
entreprises = Entreprise.objects.all()
|
||||
if request.method != "POST":
|
||||
return render(request, "compta/saisie_paiement.html", _context(entreprises))
|
||||
|
||||
entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first()
|
||||
d = request.POST.get("date") or date.today().isoformat()
|
||||
libelle = (request.POST.get("libelle") or "").strip()
|
||||
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")
|
||||
categories = request.POST.getlist("l_categorie")
|
||||
activites = request.POST.getlist("l_activite")
|
||||
regimes = request.POST.getlist("l_regime")
|
||||
taux = request.POST.getlist("l_taux")
|
||||
montants = request.POST.getlist("l_montant")
|
||||
tvas = request.POST.getlist("l_tva")
|
||||
|
||||
lignes = []
|
||||
for i in range(len(libelles)):
|
||||
m = _dec(montants[i] if i < len(montants) else "0")
|
||||
lib = (libelles[i] or "").strip()
|
||||
if m == 0 and not lib:
|
||||
continue
|
||||
lignes.append({
|
||||
"libelle": lib or "(sans libellé)",
|
||||
"categorie": cats.get(categories[i] if i < len(categories) else ""),
|
||||
"activite": activites[i] if i < len(activites) else "",
|
||||
"regime": regimes[i] if i < len(regimes) else RegimeTva.NATIONALE,
|
||||
"taux": _dec(taux[i] if i < len(taux) else "0"),
|
||||
"montant": m,
|
||||
"tva": _dec(tvas[i] if i < len(tvas) else "0"),
|
||||
})
|
||||
|
||||
somme = sum((l["montant"] for l in lignes), Decimal("0"))
|
||||
if not lignes:
|
||||
messages.error(request, "Ajoute au moins une ligne de dépense.")
|
||||
return render(request, "compta/saisie_paiement.html",
|
||||
_context(entreprises, request.POST))
|
||||
if abs(somme - total_abs) >= Decimal("0.01"):
|
||||
messages.error(request,
|
||||
f"La somme des lignes ({somme} €) ne correspond pas au montant "
|
||||
f"({total_abs} €). Écart : {somme - total_abs} €.")
|
||||
return render(request, "compta/saisie_paiement.html",
|
||||
_context(entreprises, request.POST))
|
||||
|
||||
paiement = Paiement.objects.create(
|
||||
entreprise=entreprise, date=d, libelle=libelle,
|
||||
montant_total=total_abs, sens="debit")
|
||||
for l in lignes:
|
||||
Depense.objects.create(
|
||||
entreprise=entreprise, paiement=paiement, date=d,
|
||||
libelle=l["libelle"], categorie=l["categorie"],
|
||||
activite=l["activite"] or "", regime_tva=l["regime"],
|
||||
taux_tva=l["taux"], montant_ttc=l["montant"], montant_tva=l["tva"])
|
||||
messages.success(request,
|
||||
f"Paiement « {libelle} » enregistré ({len(lignes)} dépense(s)).")
|
||||
return redirect("saisie_paiement")
|
||||
|
||||
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