JB Compta v1.1 - multi-activité
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
*.sqlite3
|
||||
__pycache__/
|
||||
*.pyc
|
||||
staticfiles/
|
||||
docs/
|
||||
*.md
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copie ce fichier en .env et renseigne tes valeurs.
|
||||
# En production (Portainer), saisis plutôt ces variables dans l'interface.
|
||||
|
||||
# Sécurité Django
|
||||
DJANGO_SECRET_KEY=change-moi-avec-une-longue-chaine-aleatoire
|
||||
DJANGO_DEBUG=0
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,192.168.1.50
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://192.168.1.50:8001
|
||||
|
||||
# PostgreSQL EXISTANT (ne pas héberger de base ici)
|
||||
DB_NAME=compta
|
||||
DB_USER=jb
|
||||
DB_PASSWORD=ton_mot_de_passe
|
||||
DB_HOST=192.168.1.40
|
||||
DB_PORT=5432
|
||||
|
||||
# Compte admin créé au 1er démarrage
|
||||
DJANGO_SUPERUSER_USERNAME=admin
|
||||
DJANGO_SUPERUSER_PASSWORD=change-moi
|
||||
DJANGO_SUPERUSER_EMAIL=admin@example.fr
|
||||
|
||||
# Données d'exemple au 1er démarrage (1 = oui, 0 = non)
|
||||
SEED_DEMO=0
|
||||
@@ -0,0 +1,8 @@
|
||||
# Forcer les fins de ligne Unix (LF) — indispensable pour les scripts shell
|
||||
# exécutés dans le conteneur Linux, même si on pousse depuis Windows.
|
||||
* text=auto eol=lf
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
entrypoint.sh text eol=lf
|
||||
build.sh text eol=lf
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Secrets & env
|
||||
.env
|
||||
*.env.local
|
||||
|
||||
# Postgres dumps
|
||||
*.dump
|
||||
*.bak
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# psycopg2-binary embarque sa propre libpq : pas besoin de build-essential ni libpq-dev.
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Fichiers statiques de l'admin (ne nécessite pas la base de données)
|
||||
RUN DJANGO_SECRET_KEY=build python manage.py collectstatic --noinput
|
||||
|
||||
EXPOSE 8000
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
@@ -0,0 +1,76 @@
|
||||
# Plan du projet — Compta micro-entreprise (Appsmith)
|
||||
|
||||
## Contexte
|
||||
|
||||
- Statut actuel : **micro-entreprise**, option **versement forfaitaire libératoire**.
|
||||
- **Assujetti à la TVA** : déclaration et paiement **mensuels** (donc hors franchise en base).
|
||||
- Trois flux payés chaque mois : **cotisations URSSAF**, **versement libératoire (impôt)**, **TVA**.
|
||||
- Outil existant : **Indy** (devis + factures + suivi bancaire) — conservé.
|
||||
- Limites d'Indy gratuit : pas de gestion comptable, **pas d'export CSV** des données.
|
||||
|
||||
## Objectif
|
||||
|
||||
Construire dans **Appsmith + PostgreSQL** une couche de gestion financière qui complète Indy :
|
||||
saisie des ventes/dépenses, calculs TVA/URSSAF/impôt, statistiques et exports CSV.
|
||||
Système **paramétrable** car le statut pourra évoluer.
|
||||
|
||||
## Périmètre
|
||||
|
||||
| Reste sur Indy | Géré dans Appsmith |
|
||||
|----------------|--------------------|
|
||||
| Devis | Saisie des ventes encaissées |
|
||||
| Factures (format Indy apprécié) | Saisie des dépenses |
|
||||
| Suivi bancaire | TVA collectée / déductible / à reverser |
|
||||
| | Cotisations URSSAF |
|
||||
| | Versement libératoire (impôt) |
|
||||
| | Statistiques + suivi des seuils |
|
||||
| | Exports CSV |
|
||||
|
||||
## Décisions actées
|
||||
|
||||
- **Indy conservé** pour devis et factures (complément, pas remplacement).
|
||||
- **Saisie manuelle** via formulaires (Indy n'exporte pas).
|
||||
- **Interface = Python / Django** (admin auto pour la saisie, logique fiscale en Python,
|
||||
multi-utilisateurs gratuit) — au lieu d'Appsmith.
|
||||
- **Déploiement = Docker + Portainer**, base PostgreSQL externe (conteneur sans état).
|
||||
- **TVA réelle**, suivi mensuel ; base de calcul URSSAF/impôt = **CA hors taxes (HT)**.
|
||||
- **Modèle paramétrable** : taux et seuils dans le modèle `Bareme` par activité/année ;
|
||||
aucun taux codé en dur.
|
||||
|
||||
## Modules
|
||||
|
||||
1. **Ventes** — formulaire (date émission, client, montant HT, taux TVA, date encaissement, statut). CA compté à l'encaissement.
|
||||
2. **Dépenses** — formulaire (date, catégorie, montant TTC, TVA déductible).
|
||||
3. **TVA** — vue mensuelle : collectée − déductible = à reverser ; export.
|
||||
4. **URSSAF** — CA encaissé × taux social (selon activité).
|
||||
5. **Impôt (versement libératoire)** — CA × taux (1 % / 1,7 % / 2,2 %).
|
||||
6. **Tableau de bord** — CA mensuel/annuel, % des plafonds, alertes seuils, top clients, trésorerie.
|
||||
|
||||
## Architecture technique
|
||||
|
||||
```
|
||||
PostgreSQL externe (données) ── App Django/Gunicorn (admin + dashboard + exports) ── Image Docker déployée via Portainer
|
||||
```
|
||||
|
||||
Cœur durable = le modèle de données + la logique fiscale en Python ; l'interface est remplaçable.
|
||||
|
||||
## Évolutivité du statut
|
||||
|
||||
Tout est rattaché à une table `entreprise` portant ses propres paramètres (activité, régime TVA, option fiscale).
|
||||
Un changement de statut (EI au réel, société) réutilise les tables ventes/dépenses/clients et n'ajoute que de nouvelles règles de calcul. Multi-entreprises prévu pour gérer d'autres personnes plus tard.
|
||||
|
||||
## Phasage
|
||||
|
||||
- **v1 — Cœur compta** : Ventes + Dépenses + calculs TVA/URSSAF/impôt + exports CSV.
|
||||
- **v2 — Pilotage** : tableau de bord, statistiques, alertes de seuils.
|
||||
- **v3 — Extension** : multi-entreprises et adaptation à un éventuel nouveau statut.
|
||||
|
||||
## Points à confirmer plus tard
|
||||
|
||||
- Régime de TVA précis (réel normal mensuel ou réel simplifié).
|
||||
- Activité fiscale exacte (BIC vente / BIC service / BNC) → fixe les bons taux.
|
||||
- Direction probable d'évolution du statut.
|
||||
|
||||
## Prochaine étape technique
|
||||
|
||||
Adapter le jeu de données d'exemple à la TVA réelle, puis construire les formulaires Ventes et Dépenses dans Appsmith (v1).
|
||||
@@ -0,0 +1,74 @@
|
||||
# JB — Compta Micro-Entreprise
|
||||
|
||||
Application **Django** de gestion comptable pour micro-entreprise(s) française(s),
|
||||
au-dessus de **PostgreSQL**. Complète Indy (qui garde devis + factures + suivi bancaire)
|
||||
en gérant ce que sa version gratuite ne fait pas : saisie des ventes/dépenses,
|
||||
calculs **TVA / URSSAF / impôt**, statistiques et **exports CSV**.
|
||||
|
||||
Conçue pour évoluer : aucun taux n'est codé en dur (table `Bareme` par activité/année),
|
||||
multi-entreprises, et logique fiscale en Python que tu contrôles.
|
||||
|
||||
## Ce que ça fait
|
||||
|
||||
- Saisie des **ventes** (factures) et **dépenses** via l'admin Django (formulaires auto).
|
||||
- **CA HT encaissé**, suivi des plafonds micro et du seuil de franchise TVA.
|
||||
- **Cotisations URSSAF**, **versement libératoire** et **TVA à reverser** calculés sur la base HT.
|
||||
- **Tableau de bord** avec graphique mensuel + montants à provisionner.
|
||||
- **Exports CSV** (ventes, TVA, URSSAF) pour tes déclarations.
|
||||
|
||||
## Déploiement avec Portainer (recommandé)
|
||||
|
||||
La base PostgreSQL est **externe** (la tienne) ; le conteneur ne contient que l'app (sans état).
|
||||
|
||||
1. Dans Portainer → **Stacks** → **Add stack** → colle le contenu de `docker-compose.yml`
|
||||
(ou pointe sur ce dépôt Git).
|
||||
2. Renseigne les variables d'environnement (voir `.env.example`) : `DB_HOST`, `DB_NAME`,
|
||||
`DB_USER`, `DB_PASSWORD`, `DJANGO_SECRET_KEY`, `DJANGO_ALLOWED_HOSTS`,
|
||||
`DJANGO_CSRF_TRUSTED_ORIGINS`, et le compte admin `DJANGO_SUPERUSER_*`.
|
||||
3. Mets `SEED_DEMO=1` au premier lancement pour charger les barèmes 2026 + données d'exemple
|
||||
(repasse-le à `0` ensuite).
|
||||
4. Déploie. Au démarrage le conteneur applique les migrations, crée l'admin, puis sert l'app.
|
||||
5. Accède à `http://IP_DU_SERVEUR:8001` → `/admin/` pour saisir, `/dashboard/` pour les stats.
|
||||
|
||||
> Prérequis côté PostgreSQL : crée la base (ex. `compta`) et un utilisateur ayant les droits dessus.
|
||||
> Les **tables sont créées automatiquement** par les migrations Django (rien à exécuter à la main).
|
||||
|
||||
## Lancer en local (développement)
|
||||
|
||||
```bash
|
||||
python -m venv .venv && source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
cp .env.example .env # renseigne DB_* (ou laisse vide pour SQLite de test)
|
||||
python manage.py migrate
|
||||
python manage.py seed_demo # barèmes 2026 + données d'exemple (optionnel)
|
||||
python manage.py createsuperuser
|
||||
python manage.py runserver
|
||||
# http://127.0.0.1:8000/
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
JB - compta/
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── Dockerfile / entrypoint.sh / docker-compose.yml # packaging Portainer
|
||||
├── .env.example
|
||||
├── config/ # projet Django (settings, urls, wsgi)
|
||||
├── compta/ # application métier
|
||||
│ ├── models.py # Entreprise, Client, Facture, Ligne, Depense, Bareme
|
||||
│ ├── admin.py # formulaires de saisie
|
||||
│ ├── services.py # calculs TVA / URSSAF / versement libératoire (base HT)
|
||||
│ ├── views.py # dashboard + exports CSV
|
||||
│ └── templates/compta/dashboard.html
|
||||
└── docs/
|
||||
├── regles-fiscales-2026.md # seuils & taux sourcés
|
||||
├── modele-donnees.md # description du schéma
|
||||
└── deploiement.md # détails Portainer / mise à jour
|
||||
```
|
||||
|
||||
## Avertissement
|
||||
|
||||
Les taux et seuils sont ceux connus pour 2026 (voir `docs/regles-fiscales-2026.md`,
|
||||
sourcé). Cet outil aide au suivi mais ne remplace pas un expert-comptable.
|
||||
Vérifie toujours les chiffres officiels sur urssaf.fr et impots.gouv.fr.
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
# Construit l'image Docker de JB Compta sur l'hôte.
|
||||
# Usage : copie ce dossier sur ton serveur Docker, puis lance : sh build.sh
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
echo "→ Construction de l'image jb-compta:latest ..."
|
||||
docker build -t jb-compta:latest .
|
||||
echo "✓ Image construite. Dans Portainer, utilise : image: jb-compta:latest"
|
||||
@@ -0,0 +1,65 @@
|
||||
from django.contrib import admin
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import (
|
||||
Bareme, Entreprise, Client, Facture, LigneFacture, Depense,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Bareme)
|
||||
class BaremeAdmin(admin.ModelAdmin):
|
||||
list_display = ("annee", "activite", "taux_urssaf", "abattement_fiscal",
|
||||
"taux_vl", "seuil_ca", "seuil_tva_base")
|
||||
list_filter = ("annee", "activite")
|
||||
|
||||
|
||||
@admin.register(Entreprise)
|
||||
class EntrepriseAdmin(admin.ModelAdmin):
|
||||
list_display = ("nom", "activite", "franchise_tva",
|
||||
"versement_liberatoire", "email")
|
||||
list_filter = ("activite", "franchise_tva", "versement_liberatoire")
|
||||
search_fields = ("nom", "siret")
|
||||
|
||||
|
||||
@admin.register(Client)
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
list_display = ("nom", "entreprise", "email", "siret")
|
||||
list_filter = ("entreprise",)
|
||||
search_fields = ("nom", "email", "siret")
|
||||
|
||||
|
||||
class LigneFactureInline(admin.TabularInline):
|
||||
model = LigneFacture
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.action(description="Marquer comme encaissée (aujourd'hui)")
|
||||
def marquer_encaissee(modeladmin, request, queryset):
|
||||
queryset.update(statut="encaissee", date_encaissement=timezone.now().date())
|
||||
|
||||
|
||||
@admin.register(Facture)
|
||||
class FactureAdmin(admin.ModelAdmin):
|
||||
list_display = ("numero", "entreprise", "client", "date_emission",
|
||||
"date_encaissement", "statut", "montant_ht",
|
||||
"montant_tva", "montant_ttc")
|
||||
list_filter = ("entreprise", "statut", "activite", "date_emission")
|
||||
search_fields = ("numero", "client__nom")
|
||||
date_hierarchy = "date_emission"
|
||||
inlines = [LigneFactureInline]
|
||||
actions = [marquer_encaissee]
|
||||
readonly_fields = ("montant_ht", "montant_tva", "montant_ttc")
|
||||
|
||||
def save_related(self, request, form, formsets, change):
|
||||
super().save_related(request, form, formsets, change)
|
||||
# Recalcule les totaux à partir des lignes après sauvegarde
|
||||
form.instance.recompute_totaux()
|
||||
|
||||
|
||||
@admin.register(Depense)
|
||||
class DepenseAdmin(admin.ModelAdmin):
|
||||
list_display = ("date", "entreprise", "libelle", "activite", "categorie",
|
||||
"montant_ttc", "montant_tva")
|
||||
list_filter = ("entreprise", "activite", "categorie", "date")
|
||||
search_fields = ("libelle", "categorie")
|
||||
date_hierarchy = "date"
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ComptaConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "compta"
|
||||
verbose_name = "Compta micro-entreprise"
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Charge les barèmes 2026 et un jeu de données d'exemple (multi-activité).
|
||||
Usage : python manage.py seed_demo
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from compta.models import (
|
||||
Bareme, Entreprise, Client, Facture, LigneFacture, Depense, Activite,
|
||||
StatutFacture,
|
||||
)
|
||||
|
||||
# Barèmes officiels 2026 (voir docs/regles-fiscales-2026.md)
|
||||
BAREMES_2026 = [
|
||||
# activite, taux_urssaf, abattement, taux_vl, seuil_ca, tva_base, tva_majore
|
||||
(Activite.VENTE, Decimal("12.30"), Decimal("71"), Decimal("1.0"),
|
||||
Decimal("203100"), Decimal("85000"), Decimal("93500")),
|
||||
(Activite.SERVICE_BIC, Decimal("21.20"), Decimal("50"), Decimal("1.7"),
|
||||
Decimal("83600"), Decimal("37500"), Decimal("41250")),
|
||||
(Activite.SERVICE_BNC, Decimal("25.60"), Decimal("34"), Decimal("2.2"),
|
||||
Decimal("83600"), Decimal("37500"), Decimal("41250")),
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Charge les barèmes 2026 et des données d'exemple."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for act, urssaf, ab, vl, sca, tb, tm in BAREMES_2026:
|
||||
Bareme.objects.update_or_create(
|
||||
activite=act, annee=2026,
|
||||
defaults=dict(taux_urssaf=urssaf, abattement_fiscal=ab,
|
||||
taux_vl=vl, seuil_ca=sca,
|
||||
seuil_tva_base=tb, seuil_tva_majore=tm),
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS("Barèmes 2026 chargés."))
|
||||
|
||||
# Entreprise multi-activité : impression 3D (Vente BIC) + aide info (Presta BIC)
|
||||
ent, _ = Entreprise.objects.get_or_create(
|
||||
nom="JB Conseil",
|
||||
defaults=dict(activite=Activite.SERVICE_BIC, franchise_tva=False,
|
||||
taux_tva_defaut=Decimal("20.00"),
|
||||
versement_liberatoire=True, email="jb@example.fr"),
|
||||
)
|
||||
|
||||
clients = {}
|
||||
for nom, mail in [("Acme SARL", "contact@acme.fr"),
|
||||
("Studio Lumiere", "hello@studiolumiere.fr"),
|
||||
("Mairie de Ville", "compta@ville.fr")]:
|
||||
c, _ = Client.objects.get_or_create(entreprise=ent, nom=nom,
|
||||
defaults=dict(email=mail))
|
||||
clients[nom] = c
|
||||
|
||||
V, S = Activite.VENTE, Activite.SERVICE_BIC
|
||||
factures = [
|
||||
# numero, client, activite, emission, encaissement, statut, lignes
|
||||
("2026-001", "Acme SARL", S, date(2026, 1, 10), date(2026, 1, 25),
|
||||
StatutFacture.ENCAISSEE, [("Dépannage informatique", 2, 600)]),
|
||||
("2026-002", "Studio Lumiere", V, date(2026, 1, 18), date(2026, 2, 5),
|
||||
StatutFacture.ENCAISSEE, [("Impression 3D - lot pièces", 1, 800)]),
|
||||
("2026-003", "Mairie de Ville", S, date(2026, 2, 2), date(2026, 2, 20),
|
||||
StatutFacture.ENCAISSEE, [("Maintenance parc", 1, 2500)]),
|
||||
("2026-004", "Acme SARL", V, date(2026, 3, 1), date(2026, 3, 15),
|
||||
StatutFacture.ENCAISSEE, [("Impression 3D - prototype", 1, 1500)]),
|
||||
("2026-005", "Studio Lumiere", S, date(2026, 3, 20), None,
|
||||
StatutFacture.ENVOYEE, [("Audit poste de travail", 1, 950)]),
|
||||
]
|
||||
for numero, client_nom, activite, emis, enc, statut, lignes in factures:
|
||||
f, created = Facture.objects.get_or_create(
|
||||
entreprise=ent, numero=numero,
|
||||
defaults=dict(client=clients[client_nom], activite=activite,
|
||||
date_emission=emis, date_encaissement=enc,
|
||||
statut=statut),
|
||||
)
|
||||
if created:
|
||||
for des, qte, pu in lignes:
|
||||
LigneFacture.objects.create(
|
||||
facture=f, designation=des, quantite=Decimal(qte),
|
||||
prix_unitaire_ht=Decimal(pu), taux_tva=Decimal("20"))
|
||||
f.recompute_totaux()
|
||||
|
||||
Depense.objects.get_or_create(
|
||||
entreprise=ent, libelle="Bobines PLA/PETG", date=date(2026, 1, 15),
|
||||
defaults=dict(categorie="Matiere", activite=Activite.VENTE,
|
||||
montant_ttc=Decimal("96.00"), montant_tva=Decimal("16.00")))
|
||||
Depense.objects.get_or_create(
|
||||
entreprise=ent, libelle="Abonnement outils", date=date(2026, 2, 10),
|
||||
defaults=dict(categorie="Logiciel", activite=Activite.SERVICE_BIC,
|
||||
montant_ttc=Decimal("34.80"), montant_tva=Decimal("5.80")))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Données d'exemple chargées."))
|
||||
@@ -0,0 +1,130 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-29 20:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from decimal import Decimal
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Entreprise',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nom', models.CharField(max_length=200)),
|
||||
('siret', models.CharField(blank=True, max_length=20)),
|
||||
('activite', models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], default='service_bnc', max_length=20)),
|
||||
('franchise_tva', models.BooleanField(default=False, help_text='Décochez si vous facturez et déclarez la TVA.', verbose_name='En franchise de TVA')),
|
||||
('taux_tva_defaut', models.DecimalField(decimal_places=2, default=Decimal('20.00'), max_digits=4, verbose_name='Taux de TVA par défaut (%)')),
|
||||
('versement_liberatoire', models.BooleanField(default=False, verbose_name='Option versement libératoire')),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('adresse', models.TextField(blank=True)),
|
||||
('cree_le', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Entreprise',
|
||||
'verbose_name_plural': 'Entreprises',
|
||||
'ordering': ['nom'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Bareme',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('activite', models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], max_length=20)),
|
||||
('annee', models.PositiveIntegerField()),
|
||||
('taux_urssaf', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Taux cotisations URSSAF (%)')),
|
||||
('abattement_fiscal', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Abattement forfaitaire (%)')),
|
||||
('taux_vl', models.DecimalField(decimal_places=2, max_digits=4, verbose_name='Taux versement libératoire (%)')),
|
||||
('seuil_ca', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Plafond CA micro (€)')),
|
||||
('seuil_tva_base', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Seuil franchise TVA (€)')),
|
||||
('seuil_tva_majore', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Seuil franchise TVA majoré (€)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Barème',
|
||||
'verbose_name_plural': 'Barèmes',
|
||||
'ordering': ['-annee', 'activite'],
|
||||
'unique_together': {('activite', 'annee')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Depense',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('categorie', models.CharField(blank=True, max_length=100)),
|
||||
('libelle', models.CharField(max_length=255)),
|
||||
('montant_ttc', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('montant_tva', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12, verbose_name='TVA déductible (€)')),
|
||||
('cree_le', models.DateTimeField(auto_now_add=True)),
|
||||
('entreprise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='depenses', to='compta.entreprise')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Dépense',
|
||||
'verbose_name_plural': 'Dépenses',
|
||||
'ordering': ['-date'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Client',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nom', models.CharField(max_length=200)),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('siret', models.CharField(blank=True, max_length=20)),
|
||||
('adresse', models.TextField(blank=True)),
|
||||
('cree_le', models.DateTimeField(auto_now_add=True)),
|
||||
('entreprise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clients', to='compta.entreprise')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Client',
|
||||
'verbose_name_plural': 'Clients',
|
||||
'ordering': ['nom'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Facture',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('numero', models.CharField(max_length=50)),
|
||||
('activite', models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], max_length=20)),
|
||||
('date_emission', models.DateField()),
|
||||
('date_encaissement', models.DateField(blank=True, help_text="À renseigner une fois la facture payée. Le CA micro est compté à l'encaissement.", null=True)),
|
||||
('statut', models.CharField(choices=[('brouillon', 'Brouillon'), ('envoyee', 'Envoyée'), ('encaissee', 'Encaissée'), ('annulee', 'Annulée')], default='brouillon', max_length=12)),
|
||||
('montant_ht', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12)),
|
||||
('montant_tva', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12)),
|
||||
('montant_ttc', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('cree_le', models.DateTimeField(auto_now_add=True)),
|
||||
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='factures', to='compta.client')),
|
||||
('entreprise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='factures', to='compta.entreprise')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Facture',
|
||||
'verbose_name_plural': 'Factures',
|
||||
'ordering': ['-date_emission', 'numero'],
|
||||
'unique_together': {('entreprise', 'numero')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LigneFacture',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('designation', models.CharField(max_length=255)),
|
||||
('quantite', models.DecimalField(decimal_places=3, default=Decimal('1'), max_digits=12)),
|
||||
('prix_unitaire_ht', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('taux_tva', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=4)),
|
||||
('facture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lignes', to='compta.facture')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ligne de facture',
|
||||
'verbose_name_plural': 'Lignes de facture',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-29 21:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('compta', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='depense',
|
||||
name='activite',
|
||||
field=models.CharField(blank=True, choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], help_text='Activité concernée (facultatif, pour la ventilation).', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='entreprise',
|
||||
name='activite',
|
||||
field=models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], default='service_bnc', help_text='Activité par défaut des nouvelles factures. Une entreprise peut avoir plusieurs activités : chaque facture/dépense porte la sienne.', max_length=20, verbose_name='Activité principale'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Modèles de données — JB Compta.
|
||||
|
||||
Conçus pour rester valables si le statut évolue : les taux et seuils
|
||||
ne sont jamais codés en dur, ils vivent dans le modèle `Bareme`
|
||||
(par activité et par année).
|
||||
"""
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
|
||||
|
||||
class Activite(models.TextChoices):
|
||||
VENTE = "vente", "Vente de marchandises (BIC)"
|
||||
SERVICE_BIC = "service_bic", "Prestation de services (BIC)"
|
||||
SERVICE_BNC = "service_bnc", "Prestation de services / libéral (BNC)"
|
||||
|
||||
|
||||
class StatutFacture(models.TextChoices):
|
||||
BROUILLON = "brouillon", "Brouillon"
|
||||
ENVOYEE = "envoyee", "Envoyée"
|
||||
ENCAISSEE = "encaissee", "Encaissée"
|
||||
ANNULEE = "annulee", "Annulée"
|
||||
|
||||
|
||||
class Bareme(models.Model):
|
||||
"""Taux et seuils officiels, par activité et par année (paramétrable)."""
|
||||
activite = models.CharField(max_length=20, choices=Activite.choices)
|
||||
annee = models.PositiveIntegerField()
|
||||
taux_urssaf = models.DecimalField(
|
||||
"Taux cotisations URSSAF (%)", max_digits=5, decimal_places=2)
|
||||
abattement_fiscal = models.DecimalField(
|
||||
"Abattement forfaitaire (%)", max_digits=5, decimal_places=2)
|
||||
taux_vl = models.DecimalField(
|
||||
"Taux versement libératoire (%)", max_digits=4, decimal_places=2)
|
||||
seuil_ca = models.DecimalField(
|
||||
"Plafond CA micro (€)", max_digits=12, decimal_places=2)
|
||||
seuil_tva_base = models.DecimalField(
|
||||
"Seuil franchise TVA (€)", max_digits=12, decimal_places=2)
|
||||
seuil_tva_majore = models.DecimalField(
|
||||
"Seuil franchise TVA majoré (€)", max_digits=12, decimal_places=2)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Barème"
|
||||
verbose_name_plural = "Barèmes"
|
||||
unique_together = ("activite", "annee")
|
||||
ordering = ["-annee", "activite"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_activite_display()} — {self.annee}"
|
||||
|
||||
|
||||
class Entreprise(models.Model):
|
||||
nom = models.CharField(max_length=200)
|
||||
siret = models.CharField(max_length=20, blank=True)
|
||||
activite = models.CharField(
|
||||
"Activité principale", max_length=20, choices=Activite.choices,
|
||||
default=Activite.SERVICE_BNC,
|
||||
help_text="Activité par défaut des nouvelles factures. "
|
||||
"Une entreprise peut avoir plusieurs activités : "
|
||||
"chaque facture/dépense porte la sienne.")
|
||||
franchise_tva = models.BooleanField(
|
||||
"En franchise de TVA", default=False,
|
||||
help_text="Décochez si vous facturez et déclarez la TVA.")
|
||||
taux_tva_defaut = models.DecimalField(
|
||||
"Taux de TVA par défaut (%)", max_digits=4, decimal_places=2,
|
||||
default=Decimal("20.00"))
|
||||
versement_liberatoire = models.BooleanField(
|
||||
"Option versement libératoire", default=False)
|
||||
email = models.EmailField(blank=True)
|
||||
adresse = models.TextField(blank=True)
|
||||
cree_le = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Entreprise"
|
||||
verbose_name_plural = "Entreprises"
|
||||
ordering = ["nom"]
|
||||
|
||||
def __str__(self):
|
||||
return self.nom
|
||||
|
||||
def bareme(self, annee):
|
||||
return Bareme.objects.filter(activite=self.activite, annee=annee).first()
|
||||
|
||||
|
||||
class Client(models.Model):
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
||||
related_name="clients")
|
||||
nom = models.CharField(max_length=200)
|
||||
email = models.EmailField(blank=True)
|
||||
siret = models.CharField(max_length=20, blank=True)
|
||||
adresse = models.TextField(blank=True)
|
||||
cree_le = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Client"
|
||||
verbose_name_plural = "Clients"
|
||||
ordering = ["nom"]
|
||||
|
||||
def __str__(self):
|
||||
return self.nom
|
||||
|
||||
|
||||
class Facture(models.Model):
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
||||
related_name="factures")
|
||||
client = models.ForeignKey(Client, on_delete=models.PROTECT,
|
||||
related_name="factures")
|
||||
numero = models.CharField(max_length=50)
|
||||
activite = models.CharField(max_length=20, choices=Activite.choices)
|
||||
date_emission = models.DateField()
|
||||
date_encaissement = models.DateField(
|
||||
null=True, blank=True,
|
||||
help_text="À renseigner une fois la facture payée. "
|
||||
"Le CA micro est compté à l'encaissement.")
|
||||
statut = models.CharField(max_length=12, choices=StatutFacture.choices,
|
||||
default=StatutFacture.BROUILLON)
|
||||
montant_ht = models.DecimalField(max_digits=12, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
montant_tva = models.DecimalField(max_digits=12, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
montant_ttc = models.DecimalField(max_digits=12, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
notes = models.TextField(blank=True)
|
||||
cree_le = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Facture"
|
||||
verbose_name_plural = "Factures"
|
||||
unique_together = ("entreprise", "numero")
|
||||
ordering = ["-date_emission", "numero"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.numero} — {self.client}"
|
||||
|
||||
def recompute_totaux(self, save=True):
|
||||
"""Recalcule HT/TVA/TTC à partir des lignes."""
|
||||
ht = Decimal("0")
|
||||
tva = Decimal("0")
|
||||
for ligne in self.lignes.all():
|
||||
l_ht = ligne.montant_ht
|
||||
ht += l_ht
|
||||
tva += (l_ht * ligne.taux_tva / Decimal("100"))
|
||||
self.montant_ht = ht.quantize(Decimal("0.01"))
|
||||
self.montant_tva = tva.quantize(Decimal("0.01"))
|
||||
self.montant_ttc = (self.montant_ht + self.montant_tva)
|
||||
if save:
|
||||
super().save(update_fields=["montant_ht", "montant_tva", "montant_ttc"])
|
||||
|
||||
|
||||
class LigneFacture(models.Model):
|
||||
facture = models.ForeignKey(Facture, on_delete=models.CASCADE,
|
||||
related_name="lignes")
|
||||
designation = models.CharField(max_length=255)
|
||||
quantite = models.DecimalField(max_digits=12, decimal_places=3,
|
||||
default=Decimal("1"))
|
||||
prix_unitaire_ht = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
taux_tva = models.DecimalField(max_digits=4, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Ligne de facture"
|
||||
verbose_name_plural = "Lignes de facture"
|
||||
|
||||
def __str__(self):
|
||||
return self.designation
|
||||
|
||||
@property
|
||||
def montant_ht(self):
|
||||
return (self.quantite * self.prix_unitaire_ht).quantize(Decimal("0.01"))
|
||||
|
||||
|
||||
class Depense(models.Model):
|
||||
entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE,
|
||||
related_name="depenses")
|
||||
date = models.DateField()
|
||||
activite = models.CharField(
|
||||
max_length=20, choices=Activite.choices, blank=True,
|
||||
help_text="Activité concernée (facultatif, pour la ventilation).")
|
||||
categorie = models.CharField(max_length=100, blank=True)
|
||||
libelle = models.CharField(max_length=255)
|
||||
montant_ttc = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
montant_tva = models.DecimalField(
|
||||
"TVA déductible (€)", max_digits=12, decimal_places=2,
|
||||
default=Decimal("0"))
|
||||
cree_le = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Dépense"
|
||||
verbose_name_plural = "Dépenses"
|
||||
ordering = ["-date"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.date} — {self.libelle}"
|
||||
|
||||
@property
|
||||
def montant_ht(self):
|
||||
return (self.montant_ttc - self.montant_tva).quantize(Decimal("0.01"))
|
||||
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Logique fiscale — JB Compta.
|
||||
|
||||
Bases de calcul : CA HORS TAXES (HT) encaissé.
|
||||
- Base des cotisations URSSAF et du versement libératoire (assujetti TVA).
|
||||
- En franchise de TVA, HT = TTC : le calcul reste correct.
|
||||
|
||||
Multi-activité : une entreprise peut exercer plusieurs activités (ex. Vente BIC
|
||||
+ Prestation BIC). Les taux URSSAF, abattements, taux de versement libératoire et
|
||||
seuils diffèrent par activité, donc tout est ventilé par activité puis totalisé.
|
||||
|
||||
TVA à reverser = TVA collectée (ventes encaissées) − TVA déductible (dépenses).
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.db.models import Sum
|
||||
from django.db.models.functions import TruncMonth
|
||||
|
||||
from .models import Facture, Depense, Bareme, StatutFacture, Activite
|
||||
|
||||
CENT = Decimal("0.01")
|
||||
|
||||
|
||||
def _q(value):
|
||||
return (value or Decimal("0")).quantize(CENT)
|
||||
|
||||
|
||||
def factures_encaissees(entreprise, annee):
|
||||
return Facture.objects.filter(
|
||||
entreprise=entreprise,
|
||||
statut=StatutFacture.ENCAISSEE,
|
||||
date_encaissement__year=annee,
|
||||
)
|
||||
|
||||
|
||||
def ca_ht_par_activite(entreprise, annee):
|
||||
"""Dict {activite: CA HT encaissé}."""
|
||||
rows = (
|
||||
factures_encaissees(entreprise, annee)
|
||||
.values("activite")
|
||||
.annotate(total=Sum("montant_ht"))
|
||||
)
|
||||
return {r["activite"]: _q(r["total"]) for r in rows}
|
||||
|
||||
|
||||
def ca_ht(entreprise, annee):
|
||||
agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_ht"))
|
||||
return _q(agg["s"])
|
||||
|
||||
|
||||
def tva_collectee(entreprise, annee):
|
||||
agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_tva"))
|
||||
return _q(agg["s"])
|
||||
|
||||
|
||||
def tva_deductible(entreprise, annee):
|
||||
agg = Depense.objects.filter(
|
||||
entreprise=entreprise, date__year=annee
|
||||
).aggregate(s=Sum("montant_tva"))
|
||||
return _q(agg["s"])
|
||||
|
||||
|
||||
def ca_ht_mensuel(entreprise, annee):
|
||||
"""Liste de 12 valeurs (index 0 = janvier) du CA HT encaissé (toutes activités)."""
|
||||
rows = (
|
||||
factures_encaissees(entreprise, annee)
|
||||
.annotate(mois=TruncMonth("date_encaissement"))
|
||||
.values("mois")
|
||||
.annotate(total=Sum("montant_ht"))
|
||||
)
|
||||
mensuel = [Decimal("0")] * 12
|
||||
for r in rows:
|
||||
if r["mois"]:
|
||||
mensuel[r["mois"].month - 1] = _q(r["total"])
|
||||
return mensuel
|
||||
|
||||
|
||||
def detail_activite(activite, ca, bareme):
|
||||
"""Calculs fiscaux pour une activité donnée."""
|
||||
label = dict(Activite.choices).get(activite, activite)
|
||||
d = {"activite": activite, "label": label, "ca_ht": ca, "bareme": bareme}
|
||||
if bareme:
|
||||
d.update({
|
||||
"taux_urssaf": bareme.taux_urssaf,
|
||||
"cotisations": _q(ca * bareme.taux_urssaf / Decimal("100")),
|
||||
"taux_vl": bareme.taux_vl,
|
||||
"versement_liberatoire": _q(ca * bareme.taux_vl / Decimal("100")),
|
||||
"abattement_fiscal": bareme.abattement_fiscal,
|
||||
"revenu_imposable": _q(
|
||||
ca * (Decimal("100") - bareme.abattement_fiscal) / Decimal("100")),
|
||||
"seuil_ca": bareme.seuil_ca,
|
||||
"pct_seuil_ca": (_q(ca / bareme.seuil_ca * Decimal("100"))
|
||||
if bareme.seuil_ca else Decimal("0")),
|
||||
"seuil_tva_base": bareme.seuil_tva_base,
|
||||
"depasse_seuil_tva": ca > bareme.seuil_tva_base,
|
||||
"depasse_seuil_ca": ca > bareme.seuil_ca,
|
||||
})
|
||||
else:
|
||||
d.update({
|
||||
"taux_urssaf": None, "cotisations": None,
|
||||
"taux_vl": None, "versement_liberatoire": None,
|
||||
"abattement_fiscal": None, "revenu_imposable": None,
|
||||
"seuil_ca": None, "pct_seuil_ca": None,
|
||||
"seuil_tva_base": None, "depasse_seuil_tva": False,
|
||||
"depasse_seuil_ca": False,
|
||||
})
|
||||
return d
|
||||
|
||||
|
||||
def synthese(entreprise, annee):
|
||||
"""Tableau de bord chiffré, ventilé par activité puis totalisé."""
|
||||
ca_par_act = ca_ht_par_activite(entreprise, annee)
|
||||
|
||||
# On inclut toujours l'activité principale, même sans CA encore.
|
||||
activites = set(ca_par_act) | {entreprise.activite}
|
||||
baremes = {b.activite: b for b in Bareme.objects.filter(annee=annee)}
|
||||
|
||||
lignes = []
|
||||
for act in sorted(activites):
|
||||
ca = ca_par_act.get(act, Decimal("0"))
|
||||
lignes.append(detail_activite(act, ca, baremes.get(act)))
|
||||
|
||||
# Totaux (on additionne ce qui est calculable)
|
||||
def somme(cle):
|
||||
vals = [l[cle] for l in lignes if l.get(cle) is not None]
|
||||
return _q(sum(vals, Decimal("0"))) if vals else None
|
||||
|
||||
tva_col = tva_collectee(entreprise, annee)
|
||||
tva_ded = tva_deductible(entreprise, annee)
|
||||
|
||||
return {
|
||||
"entreprise": entreprise,
|
||||
"annee": annee,
|
||||
"lignes_activite": lignes,
|
||||
"bareme_manquant": any(l["bareme"] is None and l["ca_ht"] for l in lignes),
|
||||
"ca_ht": ca_ht(entreprise, annee),
|
||||
"cotisations_urssaf": somme("cotisations"),
|
||||
"versement_liberatoire": (
|
||||
somme("versement_liberatoire") if entreprise.versement_liberatoire else None),
|
||||
"revenu_imposable": somme("revenu_imposable"),
|
||||
"tva_collectee": tva_col,
|
||||
"tva_deductible": tva_ded,
|
||||
"tva_a_reverser": _q(tva_col - tva_ded),
|
||||
"ca_mensuel": ca_ht_mensuel(entreprise, annee),
|
||||
}
|
||||
|
||||
|
||||
def annees_disponibles(entreprise):
|
||||
qs = (
|
||||
Facture.objects.filter(entreprise=entreprise,
|
||||
statut=StatutFacture.ENCAISSEE,
|
||||
date_encaissement__isnull=False)
|
||||
.dates("date_encaissement", "year")
|
||||
)
|
||||
return sorted({d.year for d in qs}, reverse=True)
|
||||
@@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>JB Compta — Tableau de bord</title>
|
||||
<style>
|
||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
||||
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
|
||||
background:var(--bg); color:var(--txt); padding:24px; }
|
||||
h1 { margin:0 0 4px; font-size:22px; }
|
||||
h2 { font-size:15px; color:var(--muted); margin:24px 0 10px; font-weight:600; }
|
||||
a { color:var(--accent); }
|
||||
.sub { color:var(--muted); margin-bottom:20px; }
|
||||
form.filters { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:24px; }
|
||||
select, button { background:var(--card); color:var(--txt); border:1px solid #334155;
|
||||
border-radius:8px; padding:8px 12px; font-size:14px; }
|
||||
button { cursor:pointer; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
|
||||
gap:16px; margin-bottom:8px; }
|
||||
.card { background:var(--card); border:1px solid #334155; border-radius:12px;
|
||||
padding:16px; }
|
||||
.card .label { color:var(--muted); font-size:13px; }
|
||||
.card .value { font-size:26px; font-weight:600; margin-top:6px; }
|
||||
.card .hint { color:var(--muted); font-size:12px; margin-top:4px; }
|
||||
.ok{color:var(--ok)} .warn{color:var(--warn)} .bad{color:var(--bad)}
|
||||
.bar { height:8px; background:#334155; border-radius:99px; overflow:hidden; margin-top:8px;}
|
||||
.bar > span { display:block; height:100%; background:var(--accent); }
|
||||
.row { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-top:8px; }
|
||||
@media(max-width:800px){ .row{grid-template-columns:1fr;} }
|
||||
.exports a { display:inline-block; margin-right:12px; }
|
||||
canvas { max-width:100%; }
|
||||
table { width:100%; border-collapse:collapse; font-size:14px; }
|
||||
td,th { padding:8px; border-bottom:1px solid #334155; text-align:right; }
|
||||
th:first-child, td:first-child { text-align:left; }
|
||||
tr.total td { font-weight:600; border-top:2px solid #475569; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% if not entreprise %}
|
||||
<h1>Aucune entreprise</h1>
|
||||
<p class="sub">Crée une entreprise et des factures dans
|
||||
<a href="/admin/">l'administration</a>, puis reviens ici.</p>
|
||||
{% else %}
|
||||
<h1>{{ entreprise.nom }}</h1>
|
||||
<div class="sub">{{ entreprise.get_activite_display }} (principale) ·
|
||||
{% if entreprise.franchise_tva %}Franchise de TVA{% else %}Assujetti TVA{% endif %}
|
||||
{% if entreprise.versement_liberatoire %}· Versement libératoire{% endif %}
|
||||
· Exercice {{ annee }}</div>
|
||||
|
||||
<form class="filters" method="get">
|
||||
<select name="entreprise" onchange="this.form.submit()">
|
||||
{% for e in entreprises %}
|
||||
<option value="{{ e.pk }}" {% if e.pk == entreprise.pk %}selected{% endif %}>{{ e.nom }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="annee" onchange="this.form.submit()">
|
||||
{% for a in annees %}
|
||||
<option value="{{ a }}" {% if a == annee %}selected{% endif %}>{{ a }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<a href="/admin/"><button type="button">Saisie / Admin</button></a>
|
||||
</form>
|
||||
|
||||
{% if bareme_manquant %}
|
||||
<div class="card bad" style="margin-bottom:16px;">
|
||||
Une activité avec du CA n'a pas de barème {{ annee }}. Ajoute-le dans
|
||||
<a href="/admin/compta/bareme/">Barèmes</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Totaux entreprise -->
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">CA HT encaissé (total)</div>
|
||||
<div class="value">{{ ca_ht }} €</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Cotisations URSSAF (total)</div>
|
||||
<div class="value">{{ cotisations_urssaf|default:"—" }} €</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Impôt — versement libératoire</div>
|
||||
<div class="value">{{ versement_liberatoire|default:"—" }} €</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">TVA à reverser</div>
|
||||
<div class="value">{{ tva_a_reverser }} €</div>
|
||||
<div class="hint">{{ tva_collectee }} collectée − {{ tva_deductible }} déductible</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ventilation par activité -->
|
||||
<h2>Ventilation par activité</h2>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Activité</th><th>CA HT</th><th>% plafond</th>
|
||||
<th>URSSAF</th><th>Impôt (VL)</th><th>Revenu imposable</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in lignes_activite %}
|
||||
<tr>
|
||||
<td>{{ l.label }}</td>
|
||||
<td>{{ l.ca_ht }} €</td>
|
||||
<td class="{% if l.depasse_seuil_ca %}bad{% endif %}">
|
||||
{% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>{% if l.cotisations is not None %}{{ l.cotisations }} €{% else %}—{% endif %}</td>
|
||||
<td>{% if l.versement_liberatoire is not None %}{{ l.versement_liberatoire }} €{% else %}—{% endif %}</td>
|
||||
<td>{% if l.revenu_imposable is not None %}{{ l.revenu_imposable }} €{% else %}—{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total">
|
||||
<td>Total</td>
|
||||
<td>{{ ca_ht }} €</td>
|
||||
<td></td>
|
||||
<td>{{ cotisations_urssaf|default:"—" }} €</td>
|
||||
<td>{{ versement_liberatoire|default:"—" }} €</td>
|
||||
<td>{{ revenu_imposable|default:"—" }} €</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
<div class="label" style="margin-bottom:8px;">CA HT mensuel (toutes activités)</div>
|
||||
<canvas id="caChart" height="120"></canvas>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label" style="margin-bottom:8px;">À provisionner (année)</div>
|
||||
<table>
|
||||
<tr><th>Poste</th><th>Montant</th></tr>
|
||||
<tr><td>URSSAF</td><td>{{ cotisations_urssaf|default:"—" }} €</td></tr>
|
||||
<tr><td>Impôt (VL)</td><td>{{ versement_liberatoire|default:"—" }} €</td></tr>
|
||||
<tr><td>TVA</td><td>{{ tva_a_reverser }} €</td></tr>
|
||||
</table>
|
||||
<div class="exports" style="margin-top:16px;">
|
||||
<div class="label" style="margin-bottom:6px;">Exports CSV</div>
|
||||
<a href="{% url 'export_ventes' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">Ventes</a>
|
||||
<a href="{% url 'export_tva' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">TVA</a>
|
||||
<a href="{% url 'export_urssaf' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">URSSAF</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
const ctx = document.getElementById('caChart');
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {{ mois_labels|safe }},
|
||||
datasets: [{ label: 'CA HT (€)', data: {{ ca_mensuel_json|safe }},
|
||||
backgroundColor: '#38bdf8' }]
|
||||
},
|
||||
options: { plugins:{legend:{display:false}},
|
||||
scales:{ x:{ticks:{color:'#94a3b8'}}, y:{ticks:{color:'#94a3b8'}} } }
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("dashboard/", views.dashboard, name="dashboard"),
|
||||
path("export/ventes/", views.export_ventes, name="export_ventes"),
|
||||
path("export/tva/", views.export_tva, name="export_tva"),
|
||||
path("export/urssaf/", views.export_urssaf, name="export_urssaf"),
|
||||
]
|
||||
@@ -0,0 +1,89 @@
|
||||
import csv
|
||||
from datetime import date
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
|
||||
from .models import Entreprise
|
||||
from . import services
|
||||
|
||||
|
||||
def _entreprise_courante(request):
|
||||
eid = request.GET.get("entreprise")
|
||||
qs = Entreprise.objects.all()
|
||||
if eid:
|
||||
return get_object_or_404(Entreprise, pk=eid)
|
||||
return qs.first()
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
entreprise = _entreprise_courante(request)
|
||||
if entreprise is None:
|
||||
return render(request, "compta/dashboard.html", {"entreprise": None})
|
||||
|
||||
annee = int(request.GET.get("annee", date.today().year))
|
||||
ctx = services.synthese(entreprise, annee)
|
||||
ctx.update({
|
||||
"entreprises": Entreprise.objects.all(),
|
||||
"annees": services.annees_disponibles(entreprise) or [annee],
|
||||
"mois_labels": ["Jan", "Fév", "Mar", "Avr", "Mai", "Juin",
|
||||
"Juil", "Août", "Sep", "Oct", "Nov", "Déc"],
|
||||
"ca_mensuel_json": [float(v) for v in ctx["ca_mensuel"]],
|
||||
})
|
||||
return render(request, "compta/dashboard.html", ctx)
|
||||
|
||||
|
||||
def _csv_response(filename):
|
||||
resp = HttpResponse(content_type="text/csv; charset=utf-8")
|
||||
resp["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
resp.write("") # BOM pour Excel
|
||||
return resp
|
||||
|
||||
|
||||
@login_required
|
||||
def export_ventes(request):
|
||||
entreprise = _entreprise_courante(request)
|
||||
annee = int(request.GET.get("annee", date.today().year))
|
||||
resp = _csv_response(f"ventes_{annee}.csv")
|
||||
w = csv.writer(resp, delimiter=";")
|
||||
w.writerow(["Numero", "Client", "Activite", "Date emission",
|
||||
"Date encaissement", "Statut", "HT", "TVA", "TTC"])
|
||||
for f in services.factures_encaissees(entreprise, annee).select_related("client"):
|
||||
w.writerow([f.numero, f.client.nom, f.get_activite_display(),
|
||||
f.date_emission, f.date_encaissement, f.get_statut_display(),
|
||||
f.montant_ht, f.montant_tva, f.montant_ttc])
|
||||
return resp
|
||||
|
||||
|
||||
@login_required
|
||||
def export_tva(request):
|
||||
entreprise = _entreprise_courante(request)
|
||||
annee = int(request.GET.get("annee", date.today().year))
|
||||
s = services.synthese(entreprise, annee)
|
||||
resp = _csv_response(f"tva_{annee}.csv")
|
||||
w = csv.writer(resp, delimiter=";")
|
||||
w.writerow(["Annee", "TVA collectee", "TVA deductible", "TVA a reverser"])
|
||||
w.writerow([annee, s["tva_collectee"], s["tva_deductible"],
|
||||
s["tva_a_reverser"]])
|
||||
return resp
|
||||
|
||||
|
||||
@login_required
|
||||
def export_urssaf(request):
|
||||
entreprise = _entreprise_courante(request)
|
||||
annee = int(request.GET.get("annee", date.today().year))
|
||||
s = services.synthese(entreprise, annee)
|
||||
resp = _csv_response(f"urssaf_{annee}.csv")
|
||||
w = csv.writer(resp, delimiter=";")
|
||||
w.writerow(["Annee", "Activite", "CA HT", "Taux URSSAF", "Cotisations",
|
||||
"Taux VL", "Versement liberatoire", "Abattement",
|
||||
"Revenu imposable"])
|
||||
for l in s["lignes_activite"]:
|
||||
w.writerow([annee, l["label"], l["ca_ht"], l["taux_urssaf"],
|
||||
l["cotisations"], l["taux_vl"], l["versement_liberatoire"],
|
||||
l["abattement_fiscal"], l["revenu_imposable"]])
|
||||
w.writerow([annee, "TOTAL", s["ca_ht"], "", s["cotisations_urssaf"],
|
||||
"", s["versement_liberatoire"], "", s["revenu_imposable"]])
|
||||
return resp
|
||||
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Configuration Django — JB Compta.
|
||||
Tous les paramètres sensibles viennent de variables d'environnement
|
||||
(fichier .env en développement, variables Portainer en production).
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(BASE_DIR / ".env")
|
||||
|
||||
|
||||
def env_bool(name: str, default: bool = False) -> bool:
|
||||
return os.getenv(name, str(default)).lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-insecure-change-me")
|
||||
DEBUG = env_bool("DJANGO_DEBUG", False)
|
||||
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
o for o in os.getenv("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",") if o
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"compta",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
# Base de données : PostgreSQL existant (sinon SQLite en repli pour les tests)
|
||||
if os.getenv("DB_NAME"):
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.getenv("DB_NAME"),
|
||||
"USER": os.getenv("DB_USER", "jb"),
|
||||
"PASSWORD": os.getenv("DB_PASSWORD", ""),
|
||||
"HOST": os.getenv("DB_HOST", "localhost"),
|
||||
"PORT": os.getenv("DB_PORT", "5432"),
|
||||
"CONN_MAX_AGE": 60,
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = "fr-fr"
|
||||
TIME_ZONE = "Europe/Paris"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
STORAGES = {
|
||||
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedStaticFilesStorage"},
|
||||
}
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# Redirections de connexion vers l'admin
|
||||
LOGIN_URL = "/admin/login/"
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
admin.site.site_header = "JB Compta — Administration"
|
||||
admin.site.site_title = "JB Compta"
|
||||
admin.site.index_title = "Gestion de la micro-entreprise"
|
||||
|
||||
urlpatterns = [
|
||||
path("", RedirectView.as_view(url="/dashboard/", permanent=False)),
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("compta.urls")),
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,34 @@
|
||||
# Stack à déployer dans Portainer.
|
||||
# La base PostgreSQL est EXTERNE (ta base existante) — rien à héberger ici.
|
||||
# Renseigne les variables dans Portainer (onglet "Environment variables")
|
||||
# ou via un fichier .env à côté de ce compose.
|
||||
|
||||
services:
|
||||
web:
|
||||
# OPTION A (dépôt Git) : laisse "build: ." — Portainer clone le dépôt,
|
||||
# le contexte contient tout le projet.
|
||||
# OPTION B (image pré-construite sur l'hôte) : commente "build: ." et
|
||||
# décommente la ligne "image:" ci-dessous.
|
||||
build: .
|
||||
# image: jb-compta:latest
|
||||
container_name: jb-compta
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8001:8000" # http://IP_DU_SERVEUR:8001
|
||||
environment:
|
||||
DJANGO_SECRET_KEY: "${DJANGO_SECRET_KEY}"
|
||||
DJANGO_DEBUG: "0"
|
||||
DJANGO_ALLOWED_HOSTS: "${DJANGO_ALLOWED_HOSTS}"
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: "${DJANGO_CSRF_TRUSTED_ORIGINS}"
|
||||
# --- Connexion à ton PostgreSQL existant ---
|
||||
DB_NAME: "${DB_NAME}"
|
||||
DB_USER: "${DB_USER}"
|
||||
DB_PASSWORD: "${DB_PASSWORD}"
|
||||
DB_HOST: "${DB_HOST}" # IP/host de ton serveur PostgreSQL
|
||||
DB_PORT: "${DB_PORT:-5432}"
|
||||
# --- Compte admin créé au 1er démarrage (optionnel) ---
|
||||
DJANGO_SUPERUSER_USERNAME: "${DJANGO_SUPERUSER_USERNAME:-admin}"
|
||||
DJANGO_SUPERUSER_PASSWORD: "${DJANGO_SUPERUSER_PASSWORD:-}"
|
||||
DJANGO_SUPERUSER_EMAIL: "${DJANGO_SUPERUSER_EMAIL:-admin@example.fr}"
|
||||
# --- Charger les données d'exemple au 1er démarrage (1 = oui) ---
|
||||
SEED_DEMO: "${SEED_DEMO:-0}"
|
||||
@@ -0,0 +1,83 @@
|
||||
# Déploiement (Portainer) & maintenance
|
||||
|
||||
## Principe
|
||||
|
||||
- Le conteneur ne contient que **l'application** (Gunicorn). Il est **sans état**.
|
||||
- La **base PostgreSQL est externe** (la tienne). Aucune donnée n'est stockée dans le conteneur.
|
||||
- Au démarrage, l'`entrypoint` applique les migrations, crée le compte admin (si variables
|
||||
fournies), charge éventuellement les données d'exemple, puis lance Gunicorn.
|
||||
|
||||
## Préparer PostgreSQL
|
||||
|
||||
Sur ton serveur PostgreSQL existant :
|
||||
|
||||
```sql
|
||||
CREATE DATABASE compta;
|
||||
CREATE USER jb WITH PASSWORD 'ton_mot_de_passe';
|
||||
GRANT ALL PRIVILEGES ON DATABASE compta TO jb;
|
||||
-- PostgreSQL 15+ : donner aussi les droits sur le schéma public
|
||||
\c compta
|
||||
GRANT ALL ON SCHEMA public TO jb;
|
||||
```
|
||||
|
||||
Vérifie que le serveur accepte les connexions depuis l'hôte Docker
|
||||
(`listen_addresses` dans `postgresql.conf` et une ligne dans `pg_hba.conf`).
|
||||
|
||||
## Important : le contexte de build
|
||||
|
||||
`COPY . .` dans le Dockerfile copie le **contexte de build** (les fichiers fournis à Docker).
|
||||
L'**éditeur web** de Portainer ne fournit **que le docker-compose** comme contexte : tes dossiers
|
||||
`config/` et `compta/` sont absents, et le build échoue avec `No module named 'config'`.
|
||||
Il faut donc fournir le projet complet par l'une des deux méthodes ci-dessous.
|
||||
|
||||
### Option A — dépôt Git (recommandé)
|
||||
|
||||
1. Pousse le dossier du projet dans un dépôt Git (GitHub privé, ou Gitea auto-hébergé).
|
||||
2. Portainer → **Stacks → Add stack → Repository**.
|
||||
3. Renseigne l'URL du dépôt, la branche, et `docker-compose.yml` comme *Compose path*.
|
||||
4. **Environment variables** : ajoute les valeurs (cf. `.env.example`).
|
||||
5. **Deploy the stack**. Portainer clone le dépôt → contexte complet → `build: .` fonctionne.
|
||||
|
||||
Mise à jour : `git push`, puis dans Portainer **Pull and redeploy**.
|
||||
|
||||
### Option B — image pré-construite sur l'hôte (sans Git)
|
||||
|
||||
1. Copie le projet sur l'hôte Docker (WinSCP/scp), ex. `/opt/jb-compta`.
|
||||
2. En SSH : `cd /opt/jb-compta && docker build -t jb-compta:latest .`
|
||||
3. Dans `docker-compose.yml`, commente `build: .` et décommente `image: jb-compta:latest`.
|
||||
4. Portainer → **Stacks → Add stack → Web editor**, colle le compose, renseigne les variables, déploie.
|
||||
|
||||
Mise à jour : re-copier les fichiers, `docker build` à nouveau, puis redéployer la stack.
|
||||
|
||||
### Premier démarrage
|
||||
|
||||
Mets `SEED_DEMO=1` pour charger les barèmes 2026 et un jeu d'exemple,
|
||||
puis repasse à `0` et redéploie.
|
||||
|
||||
## Accès
|
||||
|
||||
- Admin / saisie : `http://IP:8001/admin/`
|
||||
- Tableau de bord : `http://IP:8001/dashboard/`
|
||||
|
||||
Change le port `8001` dans le compose si besoin.
|
||||
|
||||
## Mettre à jour l'application
|
||||
|
||||
1. Mets à jour le code (Git pull ou nouvelle image).
|
||||
2. Dans Portainer : **Stacks → jb-compta → Update / Re-pull and redeploy**.
|
||||
3. Les migrations éventuelles s'appliquent automatiquement au redémarrage.
|
||||
|
||||
## Sauvegarde
|
||||
|
||||
Sauvegarde **uniquement la base PostgreSQL** (le conteneur n'a rien à sauvegarder) :
|
||||
|
||||
```bash
|
||||
pg_dump -h IP_DB -U jb compta > sauvegarde_compta_$(date +%F).sql
|
||||
```
|
||||
|
||||
## Sécurité
|
||||
|
||||
- Génère un vrai `DJANGO_SECRET_KEY` (longue chaîne aléatoire).
|
||||
- `DJANGO_DEBUG=0` en production.
|
||||
- Renseigne `DJANGO_ALLOWED_HOSTS` et `DJANGO_CSRF_TRUSTED_ORIGINS` avec ton IP/domaine.
|
||||
- Place l'app derrière un reverse proxy HTTPS (Nginx Proxy Manager, Traefik…) si tu l'exposes.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Modèle de données
|
||||
|
||||
Modèles Django (`compta/models.py`). Les tables sont créées par les migrations.
|
||||
|
||||
## Tables
|
||||
|
||||
- **Entreprise** — chaque micro-entreprise gérée (la tienne et celles d'autres personnes).
|
||||
Porte l'activité, le régime de TVA (`franchise_tva`), le taux de TVA par défaut et
|
||||
l'option versement libératoire.
|
||||
- **Client** — clients, rattachés à une entreprise.
|
||||
- **Facture** — factures. Le CA micro étant compté à l'encaissement, le champ clé est
|
||||
`date_encaissement` et le statut `encaissee`. Les montants HT/TVA/TTC sont recalculés
|
||||
depuis les lignes (`recompute_totaux`) puis figés pour garder la valeur historique.
|
||||
- **LigneFacture** — détail (désignation, quantité, prix unitaire HT, taux TVA).
|
||||
- **Depense** — suivi de trésorerie ; `montant_tva` sert de TVA déductible.
|
||||
- **Bareme** — taux et seuils par activité et par année (aucun taux codé en dur).
|
||||
|
||||
## Calculs (`compta/services.py`)
|
||||
|
||||
Toutes les bases s'appuient sur le **CA hors taxes (HT) encaissé** :
|
||||
|
||||
| Indicateur | Formule |
|
||||
|-----------|---------|
|
||||
| Cotisations URSSAF | CA HT × `taux_urssaf` |
|
||||
| Versement libératoire | CA HT × `taux_vl` |
|
||||
| Revenu imposable | CA HT × (1 − `abattement_fiscal`) |
|
||||
| TVA à reverser | TVA collectée (ventes encaissées) − TVA déductible (dépenses) |
|
||||
| % plafond | CA HT ÷ `seuil_ca` |
|
||||
|
||||
> Pourquoi le HT ? Quand l'entreprise est assujettie à la TVA, la base déclarée à
|
||||
> l'URSSAF et au fisc est le chiffre d'affaires **hors taxes**. En franchise, HT = TTC,
|
||||
> donc la formule reste correcte dans les deux cas.
|
||||
|
||||
## Évolutivité
|
||||
|
||||
- **Multi-entreprises** : tout est filtré par `entreprise`.
|
||||
- **Changement de statut** : on garde les tables ventes/dépenses/clients ; on ajoute
|
||||
seulement de nouvelles règles de calcul (ex. charges réelles déductibles au réel).
|
||||
- **Nouvelle année** : ajoute des `Bareme` avec `annee = 2027` ; rien d'autre à changer.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Règles fiscales micro-entreprise — 2026
|
||||
|
||||
Valeurs utilisées par l'application (table `bareme`). À revérifier chaque année sur urssaf.fr et impots.gouv.fr.
|
||||
|
||||
## Plafonds de chiffre d'affaires (régime micro)
|
||||
|
||||
Seuils revalorisés pour 2026 :
|
||||
|
||||
| Activité | Plafond CA |
|
||||
|----------|-----------|
|
||||
| Vente de marchandises / hébergement (BIC) | 203 100 € |
|
||||
| Prestations de services (BIC et BNC) | 83 600 € |
|
||||
|
||||
Au-delà, sortie du régime micro.
|
||||
|
||||
## Cotisations sociales URSSAF (taux 2026)
|
||||
|
||||
| Activité | Taux |
|
||||
|----------|------|
|
||||
| Vente de marchandises (BIC) | 12,3 % |
|
||||
| Prestations de services BIC | 21,2 % |
|
||||
| Professions libérales BNC | 25,6 % |
|
||||
| Professions libérales CIPAV | 23,2 % |
|
||||
|
||||
Au 1er janvier 2026, le taux des prestations BNC augmente de 1 point. Calcul : `cotisations = CA encaissé × taux`. (Le barème de l'app retient 25,6 % pour `service_bnc` ; ajustez à 23,2 % si l'activité relève de la CIPAV.)
|
||||
|
||||
## TVA — franchise en base (seuils 2026)
|
||||
|
||||
| Activité | Seuil de base | Seuil majoré |
|
||||
|----------|--------------|--------------|
|
||||
| Vente / hébergement | 85 000 € | 93 500 € |
|
||||
| Prestations de services | 37 500 € | 41 250 € |
|
||||
|
||||
En franchise en base : pas de TVA facturée ni collectée, pas de déclaration ; mention obligatoire sur les factures : **« TVA non applicable, article 293 B du CGI »**. En contrepartie, la TVA sur les achats n'est pas déductible.
|
||||
|
||||
Important : les seuils de franchise TVA sont distincts des plafonds du régime micro. On peut dépasser le seuil TVA tout en restant en micro-entreprise. Au-delà du seuil majoré, on devient redevable de la TVA dès le jour du dépassement.
|
||||
|
||||
## Impôt sur le revenu
|
||||
|
||||
**Abattement forfaitaire** (régime de droit commun) appliqué au CA pour obtenir le revenu imposable :
|
||||
|
||||
| Activité | Abattement |
|
||||
|----------|-----------|
|
||||
| Vente de marchandises | 71 % |
|
||||
| Prestations de services BIC | 50 % |
|
||||
| Prestations / BNC | 34 % |
|
||||
|
||||
**Versement libératoire** (option) — taux appliqué directement au CA brut, sans abattement :
|
||||
|
||||
| Activité | Taux |
|
||||
|----------|------|
|
||||
| Vente (BIC) | 1 % |
|
||||
| Prestations commerciales/artisanales (BIC) | 1,7 % |
|
||||
| Autres prestations et BNC | 2,2 % |
|
||||
|
||||
Éligibilité au versement libératoire en 2026 : revenu fiscal de référence 2024 inférieur à 29 315 € par part.
|
||||
|
||||
## Sources
|
||||
|
||||
- [Seuils TVA micro-entreprise 2026 — petite-entreprise.net](https://www.petite-entreprise.net/aucune/seuils-tva-micro-entreprise-2026.html)
|
||||
- [2026 : modification des seuils de chiffre d'affaires — autoentrepreneur.urssaf.fr](https://www.autoentrepreneur.urssaf.fr/portail/accueil/sinformer-sur-le-statut/toutes-les-actualites/2026--modification-des-seuils-de.html)
|
||||
- [Évolution des taux de cotisations sociales — urssaf.fr](https://www.urssaf.fr/accueil/actualites/taux-cotisations-autoentrepeneur.html)
|
||||
- [Micro-entreprises, montant des cotisations sociales — economie.gouv.fr](https://www.economie.gouv.fr/entreprises/gerer-sa-micro-entreprise/micro-entreprises-quel-est-le-montant-de-vos-cotisations-sociales)
|
||||
- [Le versement libératoire — impots.gouv.fr](https://www.impots.gouv.fr/professionnel/le-versement-liberatoire)
|
||||
- [Régime fiscal de la micro-entreprise — service-public.gouv.fr](https://entreprendre.service-public.gouv.fr/vosdroits/F23267)
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "→ Migrations de la base..."
|
||||
python manage.py migrate --noinput
|
||||
|
||||
# Création automatique d'un compte admin si les variables sont fournies
|
||||
if [ -n "$DJANGO_SUPERUSER_USERNAME" ] && [ -n "$DJANGO_SUPERUSER_PASSWORD" ]; then
|
||||
echo "→ Vérification du compte admin..."
|
||||
python manage.py createsuperuser --noinput 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Chargement optionnel des données d'exemple (barèmes + démo)
|
||||
if [ "$SEED_DEMO" = "1" ]; then
|
||||
echo "→ Chargement des données d'exemple..."
|
||||
python manage.py seed_demo || true
|
||||
fi
|
||||
|
||||
echo "→ Démarrage de Gunicorn sur :8000"
|
||||
exec gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
"""Utilitaire en ligne de commande Django."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Django introuvable. Active ton environnement virtuel et "
|
||||
"installe les dépendances (pip install -r requirements.txt)."
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
Django>=5.0,<5.3
|
||||
psycopg2-binary>=2.9
|
||||
gunicorn>=22.0
|
||||
python-dotenv>=1.0
|
||||
whitenoise>=6.6
|
||||
Reference in New Issue
Block a user