Compare commits
4 Commits
358cf35790
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 507ffa283f | |||
| 888bcfa213 | |||
| 757c0850c3 | |||
| 0a4ba2001b |
+10
-2
@@ -4,8 +4,16 @@
|
|||||||
# Sécurité Django
|
# Sécurité Django
|
||||||
DJANGO_SECRET_KEY=change-moi-avec-une-longue-chaine-aleatoire
|
DJANGO_SECRET_KEY=change-moi-avec-une-longue-chaine-aleatoire
|
||||||
DJANGO_DEBUG=0
|
DJANGO_DEBUG=0
|
||||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,192.168.1.50
|
# Domaine Traefik + accès direct éventuel (IP de l'hôte Docker / localhost)
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://192.168.1.50:8001
|
DJANGO_ALLOWED_HOSTS=compta.lespatas.ovh,localhost,127.0.0.1
|
||||||
|
# Origine HTTPS servie par Traefik — le schéma https:// est OBLIGATOIRE
|
||||||
|
DJANGO_CSRF_TRUSTED_ORIGINS=https://compta.lespatas.ovh
|
||||||
|
|
||||||
|
# Derrière Traefik (reverse-proxy distant qui termine le TLS) : mets 1.
|
||||||
|
# Django reconnaît alors le HTTPS via X-Forwarded-Proto et sécurise les cookies.
|
||||||
|
DJANGO_BEHIND_PROXY=1
|
||||||
|
# Laisse 0 : c'est Traefik qui redirige HTTP->HTTPS.
|
||||||
|
DJANGO_SSL_REDIRECT=0
|
||||||
|
|
||||||
# PostgreSQL EXISTANT (ne pas héberger de base ici)
|
# PostgreSQL EXISTANT (ne pas héberger de base ici)
|
||||||
DB_NAME=compta
|
DB_NAME=compta
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: Deploy via Portainer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Trigger Portainer stack redeploy
|
||||||
|
run: |
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${{ secrets.PORTAINER_WEBHOOK }}")
|
||||||
|
echo "HTTP: $HTTP_CODE"
|
||||||
|
[ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ] || exit 1
|
||||||
@@ -2,6 +2,14 @@
|
|||||||
.env
|
.env
|
||||||
*.env.local
|
*.env.local
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Django
|
||||||
|
db.sqlite3
|
||||||
|
staticfiles/
|
||||||
|
|
||||||
# Postgres dumps
|
# Postgres dumps
|
||||||
*.dump
|
*.dump
|
||||||
*.bak
|
*.bak
|
||||||
|
|||||||
+3
-3
@@ -30,10 +30,10 @@ class ScopedAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Categorie)
|
@admin.register(Categorie)
|
||||||
class CategorieAdmin(admin.ModelAdmin):
|
class CategorieAdmin(admin.ModelAdmin):
|
||||||
list_display = ("nom", "usage", "exclure_calculs", "actif", "ordre")
|
list_display = ("nom", "usage", "exclure_calculs", "compte_pcg", "actif", "ordre")
|
||||||
list_filter = ("usage", "exclure_calculs", "actif")
|
list_filter = ("usage", "exclure_calculs", "actif")
|
||||||
list_editable = ("usage", "exclure_calculs", "actif", "ordre")
|
list_editable = ("usage", "exclure_calculs", "compte_pcg", "actif", "ordre")
|
||||||
search_fields = ("nom",)
|
search_fields = ("nom", "compte_pcg")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Bareme)
|
@admin.register(Bareme)
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""
|
||||||
|
Catalogue des catégories Indy (référentiel complet), source de vérité unique
|
||||||
|
utilisée par le seed (création des Catégorie) et par la sync Indy (mapping
|
||||||
|
numéro de compte PCG -> catégorie).
|
||||||
|
|
||||||
|
Chaque entrée : (nom, usage, exclure_calculs, compte_pcg)
|
||||||
|
- usage : "vente" | "depense" | "neutre"
|
||||||
|
- exclure_calculs=True : mouvement neutre (hors calcul du bénéfice / non déductible /
|
||||||
|
trésorerie / impôts) — exclu des calculs fiscaux.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# nom, usage, exclure_calculs, compte_pcg
|
||||||
|
CATALOGUE = [
|
||||||
|
# --- Revenus (ventes) ---
|
||||||
|
("Vente de produits finis", "vente", False, "701000"),
|
||||||
|
("Prestation de services", "vente", False, "706001"),
|
||||||
|
("Vente de services annexes", "vente", False, "706002"),
|
||||||
|
("Vente de marchandise", "vente", False, "707000"),
|
||||||
|
("Autre gain divers", "vente", False, "710000"),
|
||||||
|
|
||||||
|
# --- Dépenses ---
|
||||||
|
("Matériel et outillage", "depense", False, "606000"),
|
||||||
|
("Matière première", "depense", False, ""), # (nôtre, pas de compte Indy)
|
||||||
|
("Achat", "depense", False, "605000"),
|
||||||
|
("Immobilisation", "depense", False, "210000"),
|
||||||
|
("Frais divers", "depense", False, "600000"),
|
||||||
|
("Télécom, fournitures, documentation", "depense", False, "626000"),
|
||||||
|
("Frais d'acte et de contentieux", "depense", False, "622700"),
|
||||||
|
("Honoraires payés", "depense", False, "622000"),
|
||||||
|
("Formation", "depense", False, "618000"),
|
||||||
|
("Réception et congrès", "depense", False, "625600"),
|
||||||
|
("Restaurant et repas d'affaires", "depense", False, "625700"),
|
||||||
|
("Frais de repas hors domicile", "depense", False, "625300"),
|
||||||
|
("Frais de déplacement", "depense", False, "625100"),
|
||||||
|
("Véhicule et carburant", "depense", False, "625200"),
|
||||||
|
("Location de matériel", "depense", False, "613500"),
|
||||||
|
("Loyers et charges locatives", "depense", False, "613200"),
|
||||||
|
("Entretien et réparation", "depense", False, "615000"),
|
||||||
|
("Abonnement logiciel", "depense", False, "613600"),
|
||||||
|
("Eau, gaz, électricité", "depense", False, "606100"),
|
||||||
|
("Assurance professionnelle", "depense", False, "616000"),
|
||||||
|
("Frais bancaires", "depense", False, "661000"),
|
||||||
|
("Cotisations professionnelles", "depense", False, "628100"),
|
||||||
|
("Personnel intérimaire", "depense", False, "621000"),
|
||||||
|
("[Salariés] Salaire net", "depense", False, "641000"),
|
||||||
|
("[Salariés] Charge sociale", "depense", False, "645000"),
|
||||||
|
|
||||||
|
# --- Neutres (hors calcul du bénéfice / non déductible / trésorerie / impôts) ---
|
||||||
|
("Apport personnel", "neutre", True, "108900"),
|
||||||
|
("Prélèvement personnel", "neutre", True, "108100"),
|
||||||
|
("Dépense personnelle", "neutre", True, "108200"),
|
||||||
|
("[Salariés] Prélèvement à la source", "neutre", True, "641100"),
|
||||||
|
("Amende et pénalité", "neutre", True, "108500"),
|
||||||
|
("Emprunt", "neutre", True, "164000"),
|
||||||
|
("Caution reçue", "neutre", True, "275000"),
|
||||||
|
("Caution versée", "neutre", True, ""), # même compte 275000
|
||||||
|
("Compte commun ou SCM", "neutre", True, "467000"),
|
||||||
|
("Débours pour vos clients", "neutre", True, "709800"),
|
||||||
|
("Virement interne", "neutre", True, "580000"),
|
||||||
|
("Carte à débit différé", "neutre", True, "512500"),
|
||||||
|
("Remboursement de TVA", "neutre", True, "445830"),
|
||||||
|
("Remboursement reçu", "neutre", True, ""), # (nôtre, générique)
|
||||||
|
("TVA payée", "neutre", True, "445510"),
|
||||||
|
("TVA déductible", "neutre", True, "445610"),
|
||||||
|
("TVA sur cession d'immobilisation", "neutre", True, "445775"),
|
||||||
|
("Vente d'une immobilisation", "neutre", True, "462000"),
|
||||||
|
("Cotisation sociale Urssaf", "neutre", True, "646000"),
|
||||||
|
("Cotisation retraite", "neutre", True, "646200"),
|
||||||
|
("Cotisation facultative", "neutre", True, "646100"),
|
||||||
|
("CFE", "neutre", True, "635120"),
|
||||||
|
("Autre impôt", "neutre", True, "635100"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Compte PCG -> nom de catégorie (pour la sync Indy). Premier gagnant si doublon.
|
||||||
|
COMPTE_VERS_NOM = {}
|
||||||
|
for _nom, _u, _e, _c in CATALOGUE:
|
||||||
|
if _c and _c not in COMPTE_VERS_NOM:
|
||||||
|
COMPTE_VERS_NOM[_c] = _nom
|
||||||
|
|
||||||
|
# Compte de vente -> activité (multi-activité)
|
||||||
|
COMPTE_ACTIVITE = {
|
||||||
|
"701000": "vente", "707000": "vente",
|
||||||
|
"706001": "service_bic", "706002": "service_bic",
|
||||||
|
}
|
||||||
+10
-34
@@ -1,9 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
Synchronisation Indy : récupère les transactions via l'API privée d'Indy et les
|
Synchronisation Indy : récupère les transactions via l'API privée d'Indy et les
|
||||||
normalise au format attendu par l'import (réutilisé par la page /import/).
|
normalise au format d'import.
|
||||||
|
|
||||||
La « catégorie » Indy est un numéro de compte du plan comptable (PCG) ; on le
|
La « catégorie » Indy = un numéro de compte du plan comptable (PCG). On mappe ce
|
||||||
mappe vers nos catégories. Montants en centimes, signés. `tva_intracom` => UE.
|
numéro vers nos catégories via le catalogue (categories_indy). Montants en
|
||||||
|
centimes, signés. `tva_intracom` => UE (autoliquidation).
|
||||||
|
|
||||||
API non officielle d'Indy : pour les données de l'utilisateur. Le token JWT
|
API non officielle d'Indy : pour les données de l'utilisateur. Le token JWT
|
||||||
expire (~1 h) et doit être renouvelé.
|
expire (~1 h) et doit être renouvelé.
|
||||||
@@ -12,43 +13,18 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
from .categories_indy import COMPTE_VERS_NOM, COMPTE_ACTIVITE
|
||||||
|
|
||||||
BASE_URL = "https://app.indy.fr"
|
BASE_URL = "https://app.indy.fr"
|
||||||
TRANSACTIONS_PATH = "/api/transactions/transactions-list"
|
TRANSACTIONS_PATH = "/api/transactions/transactions-list"
|
||||||
|
|
||||||
COMPTE_CATEGORIE = [
|
|
||||||
("701", "Vente de produits finis"), ("707", "Vente de produits finis"),
|
|
||||||
("706", "Prestation de services"),
|
|
||||||
("605", "Matériel et outillage"), ("606", "Matériel et outillage"),
|
|
||||||
("607", "Matériel et outillage"),
|
|
||||||
("6256", "Frais de repas hors domicile"), ("6253", "Frais de repas hors domicile"),
|
|
||||||
("626", "Télécom, fournitures, documentation"),
|
|
||||||
("6181", "Télécom, fournitures, documentation"),
|
|
||||||
("6183", "Télécom, fournitures, documentation"),
|
|
||||||
("6136", "Abonnement logiciel"), ("6226", "Abonnement logiciel"),
|
|
||||||
("646", "Cotisation sociale Urssaf"),
|
|
||||||
("44551", "TVA payée"), ("44558", "TVA payée"),
|
|
||||||
("44583", "Remboursement de TVA"),
|
|
||||||
("108", "Prélèvement personnel"),
|
|
||||||
]
|
|
||||||
|
|
||||||
COMPTE_ACTIVITE = [("701", "vente"), ("707", "vente"), ("706", "service_bic")]
|
|
||||||
|
|
||||||
|
|
||||||
def _cat(num):
|
def _cat(num):
|
||||||
num = str(num or "")
|
return COMPTE_VERS_NOM.get(str(num or ""))
|
||||||
best = None
|
|
||||||
for pref, cat in COMPTE_CATEGORIE:
|
|
||||||
if num.startswith(pref) and (best is None or len(pref) > len(best[0])):
|
|
||||||
best = (pref, cat)
|
|
||||||
return best[1] if best else None
|
|
||||||
|
|
||||||
|
|
||||||
def _act(num):
|
def _act(num):
|
||||||
num = str(num or "")
|
return COMPTE_ACTIVITE.get(str(num or ""), "")
|
||||||
for pref, act in COMPTE_ACTIVITE:
|
|
||||||
if num.startswith(pref):
|
|
||||||
return act
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def normaliser(tx):
|
def normaliser(tx):
|
||||||
@@ -109,12 +85,12 @@ def fetch_transactions(token, date_debut=None, date_fin=None, search="", page=1,
|
|||||||
return r.status_code, [], (
|
return r.status_code, [], (
|
||||||
"Reponse non-JSON (probablement Cloudflare). Le conteneur n'arrive "
|
"Reponse non-JSON (probablement Cloudflare). Le conteneur n'arrive "
|
||||||
"pas a joindre Indy directement. Recupere le CSV depuis ton poste "
|
"pas a joindre Indy directement. Recupere le CSV depuis ton poste "
|
||||||
"(MCP/CLI) et colle-le ci-dessous.")
|
"(favori/MCP) et colle-le ci-dessous.")
|
||||||
data = r.json()
|
data = r.json()
|
||||||
return 200, data.get("transactions", []), None
|
return 200, data.get("transactions", []), None
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
return None, [], ("Delai depasse en joignant Indy : le conteneur n'a peut-etre "
|
return None, [], ("Delai depasse en joignant Indy : le conteneur n'a peut-etre "
|
||||||
"pas d'acces Internet, ou Cloudflare bloque. Utilise le CSV "
|
"pas d'acces Internet, ou Cloudflare bloque. Utilise le favori "
|
||||||
"depuis ton poste.")
|
"depuis ton poste.")
|
||||||
except Exception as ex: # noqa: BLE001
|
except Exception as ex: # noqa: BLE001
|
||||||
return None, [], f"{type(ex).__name__}: {ex}"
|
return None, [], f"{type(ex).__name__}: {ex}"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Données de RÉFÉRENCE (barèmes + catégories + groupe Gestionnaire) — sans démo.
|
Données de RÉFÉRENCE (barèmes + catégories complètes Indy + groupe Gestionnaire).
|
||||||
Idempotent : sûr à relancer en production. Appelé par l'entrypoint au démarrage.
|
Idempotent : sûr à relancer en production. Appelé par l'entrypoint au démarrage.
|
||||||
|
|
||||||
Usage : python manage.py seed_reference
|
Usage : python manage.py seed_reference
|
||||||
@@ -10,6 +10,7 @@ from django.core.management.base import BaseCommand
|
|||||||
from django.contrib.auth.models import Group, Permission
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
|
||||||
from compta.models import Bareme, Categorie, Activite
|
from compta.models import Bareme, Categorie, Activite
|
||||||
|
from compta.categories_indy import CATALOGUE
|
||||||
|
|
||||||
BAREMES_2026 = [
|
BAREMES_2026 = [
|
||||||
(Activite.VENTE, Decimal("12.30"), Decimal("71"), Decimal("1.0"),
|
(Activite.VENTE, Decimal("12.30"), Decimal("71"), Decimal("1.0"),
|
||||||
@@ -20,26 +21,10 @@ BAREMES_2026 = [
|
|||||||
Decimal("83600"), Decimal("37500"), Decimal("41250")),
|
Decimal("83600"), Decimal("37500"), Decimal("41250")),
|
||||||
]
|
]
|
||||||
|
|
||||||
CATEGORIES = [
|
|
||||||
("Vente de produits finis", "vente", False, 1),
|
|
||||||
("Prestation de services", "vente", False, 2),
|
|
||||||
("Matériel et outillage", "depense", False, 1),
|
|
||||||
("Matière première", "depense", False, 2),
|
|
||||||
("Télécom, fournitures, documentation", "depense", False, 3),
|
|
||||||
("Frais de repas hors domicile", "depense", False, 4),
|
|
||||||
("Abonnement logiciel", "depense", False, 5),
|
|
||||||
("TVA payée", "neutre", True, 1),
|
|
||||||
("Cotisation sociale Urssaf", "neutre", True, 2),
|
|
||||||
("Remboursement de TVA", "neutre", True, 3),
|
|
||||||
("Prélèvement personnel", "neutre", True, 4),
|
|
||||||
("Virement interne", "neutre", True, 5),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Permissions du groupe "Gestionnaire entreprise"
|
|
||||||
GROUPE_NOM = "Gestionnaire entreprise"
|
GROUPE_NOM = "Gestionnaire entreprise"
|
||||||
PERMS_COMPLETES = ["client", "facture", "lignefacture", "depense", "paiement"] # add/change/delete/view
|
PERMS_COMPLETES = ["client", "facture", "lignefacture", "depense", "paiement"]
|
||||||
PERMS_CHANGE = ["entreprise"] # view + change (pas add/delete, ni gestion des accès)
|
PERMS_CHANGE = ["entreprise"]
|
||||||
PERMS_VUE = ["categorie", "bareme"] # view seulement
|
PERMS_VUE = ["categorie", "bareme"]
|
||||||
|
|
||||||
|
|
||||||
def charger_groupes():
|
def charger_groupes():
|
||||||
@@ -67,9 +52,10 @@ def charger_reference(stdout=None, style=None):
|
|||||||
defaults=dict(taux_urssaf=urssaf, abattement_fiscal=ab, taux_vl=vl,
|
defaults=dict(taux_urssaf=urssaf, abattement_fiscal=ab, taux_vl=vl,
|
||||||
seuil_ca=sca, seuil_tva_base=tb, seuil_tva_majore=tm))
|
seuil_ca=sca, seuil_tva_base=tb, seuil_tva_majore=tm))
|
||||||
cat = {}
|
cat = {}
|
||||||
for nom, usage, excl, ordre in CATEGORIES:
|
for i, (nom, usage, excl, compte) in enumerate(CATALOGUE):
|
||||||
c, _ = Categorie.objects.update_or_create(
|
c, _ = Categorie.objects.update_or_create(
|
||||||
nom=nom, defaults=dict(usage=usage, exclure_calculs=excl, ordre=ordre))
|
nom=nom, defaults=dict(usage=usage, exclure_calculs=excl,
|
||||||
|
compte_pcg=compte, ordre=i))
|
||||||
cat[nom] = c
|
cat[nom] = c
|
||||||
g = charger_groupes()
|
g = charger_groupes()
|
||||||
if stdout and style:
|
if stdout and style:
|
||||||
@@ -80,7 +66,7 @@ def charger_reference(stdout=None, style=None):
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Charge barèmes, catégories et le groupe Gestionnaire (sans démo)."
|
help = "Charge barèmes, catégories (référentiel Indy) et le groupe Gestionnaire."
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
charger_reference(self.stdout, self.style)
|
charger_reference(self.stdout, self.style)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.15 on 2026-07-11 17:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('compta', '0009_depense_indy_id_entreprise_indy_token_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='categorie',
|
||||||
|
name='compte_pcg',
|
||||||
|
field=models.CharField(blank=True, db_index=True, help_text='Numéro de compte du plan comptable, pour le mapping Indy.', max_length=10, verbose_name='Compte PCG (Indy)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -40,6 +40,9 @@ class Categorie(models.Model):
|
|||||||
"Neutre (exclue des calculs)", default=False,
|
"Neutre (exclue des calculs)", default=False,
|
||||||
help_text="Cochez pour les mouvements non fiscaux : TVA payée, "
|
help_text="Cochez pour les mouvements non fiscaux : TVA payée, "
|
||||||
"cotisations URSSAF, remboursement de TVA, virements…")
|
"cotisations URSSAF, remboursement de TVA, virements…")
|
||||||
|
compte_pcg = models.CharField(
|
||||||
|
"Compte PCG (Indy)", max_length=10, blank=True, db_index=True,
|
||||||
|
help_text="Numéro de compte du plan comptable, pour le mapping Indy.")
|
||||||
actif = models.BooleanField(default=True)
|
actif = models.BooleanField(default=True)
|
||||||
ordre = models.IntegerField(default=0)
|
ordre = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
|||||||
@@ -5,82 +5,188 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>JB Compta — {% block titre %}Tableau de bord{% endblock %}</title>
|
<title>JB Compta — {% block titre %}Tableau de bord{% endblock %}</title>
|
||||||
<style>
|
<style>
|
||||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
:root { --bg:#0b1120; --panel:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
||||||
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
|
--accent:#38bdf8; --accent2:#0ea5e9; --ok:#34d399; --warn:#fbbf24; --bad:#f87171;
|
||||||
|
--line:#334155; --sbw:236px; --sbw-c:68px; }
|
||||||
* { box-sizing:border-box; }
|
* { box-sizing:border-box; }
|
||||||
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
|
html,body { margin:0; }
|
||||||
background:var(--bg); color:var(--txt); padding:24px; }
|
body { font-family:system-ui,Segoe UI,Roboto,sans-serif; background:var(--bg); color:var(--txt); }
|
||||||
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; }
|
a { color:var(--accent); text-decoration:none; }
|
||||||
.sub { color:var(--muted); margin-bottom:16px; }
|
h1 { margin:0 0 4px; font-size:22px; }
|
||||||
.nav { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
|
h2 { font-size:15px; color:var(--muted); margin:22px 0 10px; font-weight:600; }
|
||||||
.nav a { padding:7px 13px; border-radius:8px; border:1px solid #334155;
|
.sub { color:var(--muted); margin-bottom:16px; font-size:14px; }
|
||||||
background:var(--card); font-size:14px; }
|
.muted { color:var(--muted); }
|
||||||
.nav a.on { background:var(--accent); color:#04263a; border-color:var(--accent); font-weight:600; }
|
.app { display:flex; min-height:100vh; }
|
||||||
.nav a.saisie { margin-left:auto; }
|
.sidebar { width:var(--sbw); flex:0 0 var(--sbw); background:var(--panel);
|
||||||
form.filters { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:20px; }
|
border-right:1px solid var(--line); position:fixed; inset:0 auto 0 0; height:100vh;
|
||||||
select, button, input { background:var(--card); color:var(--txt); border:1px solid #334155;
|
display:flex; flex-direction:column; padding:18px 14px; z-index:60;
|
||||||
border-radius:8px; padding:8px 12px; font-size:14px; }
|
transition:transform .22s ease, width .18s ease, flex-basis .18s ease, padding .18s ease;
|
||||||
button { cursor:pointer; }
|
overflow-x:hidden; overflow-y:auto; }
|
||||||
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:16px; margin-bottom:8px; }
|
.brand { font-size:18px; font-weight:700; letter-spacing:.3px; padding:6px 10px 16px;
|
||||||
.card { background:var(--card); border:1px solid #334155; border-radius:12px; padding:16px; }
|
display:flex; align-items:center; gap:10px; white-space:nowrap; }
|
||||||
.card .label { color:var(--muted); font-size:13px; }
|
.brand .dot { width:12px; height:12px; border-radius:4px; flex:0 0 12px;
|
||||||
.card .value { font-size:24px; font-weight:600; margin-top:6px; }
|
background:linear-gradient(135deg,var(--accent),var(--accent2)); }
|
||||||
|
.collapse-btn { margin-left:auto; background:transparent; border:none; color:var(--muted);
|
||||||
|
cursor:pointer; padding:4px; border-radius:7px; display:flex; }
|
||||||
|
.collapse-btn:hover { color:var(--txt); background:#1e293b; }
|
||||||
|
.collapse-btn svg { width:18px; height:18px; stroke:currentColor; fill:none; stroke-width:2; transition:transform .18s ease; }
|
||||||
|
.sidebar nav { display:flex; flex-direction:column; gap:3px; }
|
||||||
|
.sidebar nav a { display:flex; align-items:center; gap:11px; padding:10px 12px; border-radius:9px;
|
||||||
|
color:var(--txt); font-size:14px; white-space:nowrap; }
|
||||||
|
.sidebar nav a:hover { background:#1e293b; }
|
||||||
|
.sidebar nav a.on { background:linear-gradient(135deg,var(--accent),var(--accent2)); color:#04263a; font-weight:600; }
|
||||||
|
.sidebar nav a svg { width:18px; height:18px; flex:0 0 18px; stroke:currentColor; fill:none; stroke-width:1.8; }
|
||||||
|
.sidebar .sep { height:1px; background:var(--line); margin:12px 6px; }
|
||||||
|
.sidebar .foot { margin-top:auto; padding-top:12px; }
|
||||||
|
.sidebar .foot .who { font-size:12px; color:var(--muted); padding:0 12px 8px; white-space:nowrap; overflow:hidden; }
|
||||||
|
.sidebar .foot button { width:100%; background:var(--card); color:var(--txt); border:1px solid var(--line);
|
||||||
|
border-radius:9px; padding:9px 12px; font-size:14px; cursor:pointer;
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:8px; white-space:nowrap; }
|
||||||
|
.sidebar .foot button:hover { border-color:var(--accent); }
|
||||||
|
.sidebar .foot button svg { width:18px; height:18px; flex:0 0 18px; stroke:currentColor; fill:none; stroke-width:1.8; }
|
||||||
|
.main { flex:1 1 auto; margin-left:var(--sbw); padding:26px 30px; max-width:100%; min-width:0;
|
||||||
|
transition:margin-left .18s ease; }
|
||||||
|
.topbar-mobile { display:none; }
|
||||||
|
@media(min-width:821px){
|
||||||
|
body.sb-collapsed .sidebar { width:var(--sbw-c); flex-basis:var(--sbw-c); padding:18px 10px; }
|
||||||
|
body.sb-collapsed .main { margin-left:var(--sbw-c); }
|
||||||
|
body.sb-collapsed .sidebar .lbl,
|
||||||
|
body.sb-collapsed .sidebar .who { display:none; }
|
||||||
|
body.sb-collapsed .sidebar nav a,
|
||||||
|
body.sb-collapsed .sidebar .foot button { justify-content:center; padding-left:0; padding-right:0; gap:0; }
|
||||||
|
body.sb-collapsed .brand { justify-content:center; padding:6px 0 16px; }
|
||||||
|
body.sb-collapsed .collapse-btn { position:absolute; top:14px; right:8px; }
|
||||||
|
body.sb-collapsed .collapse-btn svg { transform:rotate(180deg); }
|
||||||
|
body.sb-collapsed .sidebar:hover { width:var(--sbw); flex-basis:var(--sbw); padding:18px 14px;
|
||||||
|
box-shadow:6px 0 34px #0009; }
|
||||||
|
body.sb-collapsed .sidebar:hover .lbl { display:inline; }
|
||||||
|
body.sb-collapsed .sidebar:hover .who { display:block; }
|
||||||
|
body.sb-collapsed .sidebar:hover nav a,
|
||||||
|
body.sb-collapsed .sidebar:hover .foot button { justify-content:flex-start; padding:10px 12px; gap:11px; }
|
||||||
|
body.sb-collapsed .sidebar:hover .foot button { justify-content:center; }
|
||||||
|
body.sb-collapsed .sidebar:hover .brand { justify-content:flex-start; padding:6px 10px 16px; }
|
||||||
|
body.sb-collapsed .sidebar:hover .collapse-btn { position:static; }
|
||||||
|
}
|
||||||
|
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:14px; margin-bottom:10px; }
|
||||||
|
.card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:16px; }
|
||||||
|
.card .label { color:var(--muted); font-size:12.5px; }
|
||||||
|
.card .value { font-size:23px; font-weight:600; margin-top:5px; }
|
||||||
.card .hint { color:var(--muted); font-size:12px; margin-top:4px; }
|
.card .hint { color:var(--muted); font-size:12px; margin-top:4px; }
|
||||||
.ok{color:var(--ok)} .warn{color:var(--warn)} .bad{color:var(--bad)}
|
.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; }
|
.pos{color:var(--ok)} .neg{color:var(--bad)}
|
||||||
|
select, button, input, textarea { background:var(--card); color:var(--txt); border:1px solid var(--line);
|
||||||
|
border-radius:9px; padding:9px 12px; font-size:14px; font-family:inherit; }
|
||||||
|
button { cursor:pointer; }
|
||||||
|
.btn-primary { background:linear-gradient(135deg,var(--accent),var(--accent2)); color:#04263a;
|
||||||
|
border:none; font-weight:600; }
|
||||||
|
.btn-danger { border-color:#7a2f2f; color:var(--bad); background:var(--card); }
|
||||||
|
form.filters { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:20px; align-items:end; }
|
||||||
|
form.filters label { display:flex; flex-direction:column; gap:4px; font-size:12px; color:var(--muted); }
|
||||||
|
.bar { height:8px; background:var(--line); border-radius:99px; overflow:hidden; margin-top:8px; }
|
||||||
.bar > span { display:block; height:100%; background:var(--accent); }
|
.bar > span { display:block; height:100%; background:var(--accent); }
|
||||||
.row { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-top:8px; }
|
.row { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-top:8px; }
|
||||||
@media(max-width:800px){ .row{grid-template-columns:1fr;} }
|
@media(max-width:900px){ .row{grid-template-columns:1fr;} }
|
||||||
canvas { max-width:100%; }
|
canvas { max-width:100%; }
|
||||||
|
.table-wrap { overflow-x:auto; border-radius:12px; }
|
||||||
table { width:100%; border-collapse:collapse; font-size:14px; }
|
table { width:100%; border-collapse:collapse; font-size:14px; }
|
||||||
td,th { padding:8px; border-bottom:1px solid #334155; text-align:right; }
|
td,th { padding:9px 11px; border-bottom:1px solid var(--line); text-align:right; }
|
||||||
th:first-child, td:first-child { text-align:left; }
|
th:first-child, td:first-child { text-align:left; }
|
||||||
|
th { color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.03em; }
|
||||||
tr.total td { font-weight:600; border-top:2px solid #475569; }
|
tr.total td { font-weight:600; border-top:2px solid #475569; }
|
||||||
.exports a { display:inline-block; margin-right:12px; }
|
.exports a { display:inline-block; margin-right:12px; }
|
||||||
|
.msg { padding:11px 15px; border-radius:10px; margin-bottom:12px; background:#14532d33; border:1px solid #2f6b52; }
|
||||||
|
.msg.error, .msg.bad { background:#7f1d1d33; border-color:#7a2f2f; }
|
||||||
|
@media(max-width:820px){
|
||||||
|
.sidebar { transform:translateX(-100%); box-shadow:0 0 40px #000a; }
|
||||||
|
body.sb-open .sidebar { transform:translateX(0); }
|
||||||
|
.main { margin-left:0; padding:14px 16px 40px; }
|
||||||
|
.collapse-btn { display:none; }
|
||||||
|
.topbar-mobile { display:flex; align-items:center; gap:12px; margin-bottom:14px;
|
||||||
|
position:sticky; top:0; background:var(--bg); padding:8px 0; z-index:40; }
|
||||||
|
.topbar-mobile .burger { background:var(--card); border:1px solid var(--line); border-radius:9px;
|
||||||
|
padding:8px 12px; font-size:18px; line-height:1; cursor:pointer; color:var(--txt); }
|
||||||
|
.topbar-mobile .t { font-weight:600; }
|
||||||
|
.sb-backdrop { display:none; position:fixed; inset:0; background:#020617cc; z-index:55; }
|
||||||
|
body.sb-open .sb-backdrop { display:block; }
|
||||||
|
form.filters label { flex:1 1 44%; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
{% block extrastyle %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script>try{if(localStorage.getItem('jbc_sb')==='1')document.body.classList.add('sb-collapsed');}catch(e){}</script>
|
||||||
{% if not entreprise %}
|
<div class="app">
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<div class="brand">
|
||||||
|
<span class="dot"></span><span class="lbl">JB Compta</span>
|
||||||
|
<button class="collapse-btn" onclick="toggleSidebar()" title="Réduire / déplier le menu" aria-label="Réduire le menu">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M15 6l-6 6 6 6"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<a class="{% if vue == 'global' %}on{% endif %}" title="Tableau de bord" href="/dashboard/{% if entreprise %}?entreprise={{ entreprise.pk }}{% endif %}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3 12h7V3H3zM14 21h7v-9h-7zM3 21h7v-6H3zM14 3v6h7V3z"/></svg><span class="lbl">Tableau de bord</span></a>
|
||||||
|
<a class="{% if vue == 'mensuel' %}on{% endif %}" title="Mensuel" href="/dashboard/mensuel/{% if entreprise %}?entreprise={{ entreprise.pk }}{% endif %}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M4 5h16v16H4zM4 9h16M8 3v4M16 3v4"/></svg><span class="lbl">Mensuel</span></a>
|
||||||
|
<a class="{% if vue == 'urssaf' %}on{% endif %}" title="Déclaration URSSAF" href="/declaration/urssaf/{% if entreprise %}?entreprise={{ entreprise.pk }}{% endif %}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M6 3h9l5 5v13H6zM14 3v6h6M9 13h6M9 17h6"/></svg><span class="lbl">Déclaration URSSAF</span></a>
|
||||||
|
<a class="{% if vue == 'tva' %}on{% endif %}" title="Déclaration TVA" href="/declaration/tva/{% if entreprise %}?entreprise={{ entreprise.pk }}{% endif %}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M6 3h9l5 5v13H6zM14 3v6h6M9 14l6 0M9 17l4 0"/></svg><span class="lbl">Déclaration TVA</span></a>
|
||||||
|
<a class="{% if vue == 'gestion' %}on{% endif %}" title="Entrées / sorties" href="/gestion/{% if entreprise %}?entreprise={{ entreprise.pk }}{% endif %}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18M7 4v3M17 14v3"/></svg><span class="lbl">Entrées / sorties</span></a>
|
||||||
|
<div class="sep"></div>
|
||||||
|
<a class="{% if vue == 'saisie' %}on{% endif %}" title="Saisir" href="/saisie/">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg><span class="lbl">Saisir</span></a>
|
||||||
|
<a class="{% if vue == 'import' %}on{% endif %}" title="Importer" href="/import/">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 3v12M8 11l4 4 4-4M5 21h14"/></svg><span class="lbl">Importer</span></a>
|
||||||
|
{% if user.is_staff %}<a title="Admin" href="/admin/">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 3l7 4v5c0 5-3 7-7 9-4-2-7-4-7-9V7z"/></svg><span class="lbl">Admin</span></a>{% endif %}
|
||||||
|
</nav>
|
||||||
|
<div class="foot">
|
||||||
|
{% if user.is_authenticated %}<div class="who">Connecté : {{ user.username }}</div>{% endif %}
|
||||||
|
<form method="post" action="/logout/">{% csrf_token %}<button type="submit" title="Déconnexion">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg><span class="lbl">Déconnexion</span></button></form>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div class="sb-backdrop" onclick="document.body.classList.remove('sb-open')"></div>
|
||||||
|
<main class="main">
|
||||||
|
<div class="topbar-mobile">
|
||||||
|
<button class="burger" onclick="document.body.classList.toggle('sb-open')" aria-label="Menu">☰</button>
|
||||||
|
<span class="t">JB Compta</span>
|
||||||
|
</div>
|
||||||
|
{% block topbar %}
|
||||||
|
{% if entreprise %}
|
||||||
|
<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">
|
||||||
|
<label>Entreprise
|
||||||
|
<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></label>
|
||||||
|
<label>Année
|
||||||
|
<select name="annee" onchange="this.form.submit()">
|
||||||
|
{% for a in annees %}<option value="{{ a }}" {% if a == annee %}selected{% endif %}>{{ a }}</option>{% endfor %}
|
||||||
|
</select></label>
|
||||||
|
{% block extrafilters %}{% endblock %}
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
<h1>Aucune entreprise</h1>
|
<h1>Aucune entreprise</h1>
|
||||||
<p class="sub">Crée une entreprise dans <a href="/admin/">l'administration</a>,
|
<p class="sub">Crée une entreprise dans <a href="/admin/">l'administration</a>,
|
||||||
ou <a href="/saisie/">saisis une opération</a>.</p>
|
ou <a href="/saisie/">saisis une opération</a>.</p>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<div class="nav">
|
{% endblock %}
|
||||||
<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="/import/">Importer</a>
|
|
||||||
{% if user.is_staff %}<a href="/admin/">Admin</a>{% endif %}
|
|
||||||
<form method="post" action="/logout/" style="display:inline;margin:0;">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit" style="background:var(--card);color:var(--txt);border:1px solid #334155;border-radius:8px;padding:7px 13px;font-size:14px;cursor:pointer;">Déconnexion</button>
|
|
||||||
</form>
|
|
||||||
</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 %}
|
{% block content %}{% endblock %}
|
||||||
{% endif %}
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function toggleSidebar(){
|
||||||
|
var c = document.body.classList.toggle('sb-collapsed');
|
||||||
|
try{ localStorage.setItem('jbc_sb', c ? '1' : '0'); }catch(e){}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
{% extends "compta/base.html" %}
|
||||||
|
{% load l10n %}
|
||||||
|
{% block titre %}Entrées / sorties{% endblock %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
<style>
|
||||||
|
.tag { font-size:11px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); white-space:nowrap; }
|
||||||
|
.tag.recette { color:var(--ok); border-color:#2f6b52; }
|
||||||
|
.tag.depense { color:var(--warn); border-color:#7a5f1f; }
|
||||||
|
.tag.operation { color:var(--accent); border-color:#25566e; }
|
||||||
|
.neutre-badge { font-size:10px; color:var(--muted); border:1px dashed var(--line); border-radius:6px; padding:1px 5px; margin-left:6px; }
|
||||||
|
.btn-mini { padding:5px 11px; font-size:12.5px; border-radius:8px; }
|
||||||
|
#gtable td, #gtable th { border-bottom:1px solid var(--line); }
|
||||||
|
#gtable tbody tr:hover td { background:#243247; }
|
||||||
|
.chkcol { width:34px; text-align:center !important; }
|
||||||
|
.chkcol input { width:16px; height:16px; accent-color:var(--accent); }
|
||||||
|
.empty { color:var(--muted); padding:28px; text-align:center; }
|
||||||
|
|
||||||
|
.bulkbar { position:sticky; bottom:0; margin-top:14px; display:none; gap:12px; align-items:center;
|
||||||
|
background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:12px 16px; }
|
||||||
|
.bulkbar.on { display:flex; }
|
||||||
|
.bulkbar .count { font-weight:600; }
|
||||||
|
|
||||||
|
/* modal */
|
||||||
|
.overlay { position:fixed; inset:0; background:#020617d0; display:none; align-items:center;
|
||||||
|
justify-content:center; padding:16px; z-index:100; }
|
||||||
|
.overlay.on { display:flex; }
|
||||||
|
.modal { background:var(--card); border:1px solid var(--line); border-radius:16px;
|
||||||
|
width:min(560px,100%); max-height:92vh; overflow:auto; padding:22px; }
|
||||||
|
.modal h3 { margin:0 0 16px; font-size:18px; }
|
||||||
|
.modal .field { display:flex; flex-direction:column; gap:5px; margin-bottom:13px; }
|
||||||
|
.modal .field label { font-size:13px; color:var(--muted); }
|
||||||
|
.modal .field input, .modal .field select { width:100%; }
|
||||||
|
.modal .actions { display:flex; gap:10px; justify-content:space-between; margin-top:20px; flex-wrap:wrap; }
|
||||||
|
.explic { background:#0b1220; border:1px solid var(--line); border-radius:10px;
|
||||||
|
padding:11px 13px; font-size:13px; color:var(--muted); }
|
||||||
|
.explic b { color:var(--txt); }
|
||||||
|
.hidegroup { display:none; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block topbar %}
|
||||||
|
<h1>Entrées / sorties</h1>
|
||||||
|
<div class="sub">{{ entreprise.nom }} — vue unifiée de toutes les opérations (recettes, dépenses, mouvements)</div>
|
||||||
|
|
||||||
|
{% for m in messages %}<div class="msg {{ m.tags }}">{{ m }}</div>{% endfor %}
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card"><div class="label">Opérations</div><div class="value">{{ nb }}</div></div>
|
||||||
|
<div class="card"><div class="label">Entrées</div><div class="value pos">{{ entrees|floatformat:2|unlocalize }} €</div></div>
|
||||||
|
<div class="card"><div class="label">Sorties</div><div class="value neg">{{ sorties|floatformat:2|unlocalize }} €</div></div>
|
||||||
|
<div class="card"><div class="label">Solde</div><div class="value {% if total >= 0 %}pos{% else %}neg{% endif %}">{{ total|floatformat:2|unlocalize }} €</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="filters" method="get">
|
||||||
|
<label>Entreprise
|
||||||
|
<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></label>
|
||||||
|
<label>Recherche<input type="text" name="q" value="{{ filtres.q }}" placeholder="libellé, catégorie…"></label>
|
||||||
|
<label>Type<select name="type">
|
||||||
|
<option value="">Tous</option>
|
||||||
|
<option value="recette" {% if filtres.type == 'recette' %}selected{% endif %}>Recettes</option>
|
||||||
|
<option value="depense" {% if filtres.type == 'depense' %}selected{% endif %}>Dépenses</option>
|
||||||
|
<option value="operation" {% if filtres.type == 'operation' %}selected{% endif %}>Opérations</option>
|
||||||
|
</select></label>
|
||||||
|
<label>Catégorie<select name="cat">
|
||||||
|
<option value="">Toutes</option>
|
||||||
|
{% for c in categories %}<option value="{{ c.pk }}" {% if filtres.cat == c.pk|stringformat:'s' %}selected{% endif %}>{{ c.nom }}</option>{% endfor %}
|
||||||
|
</select></label>
|
||||||
|
<label>Régime<select name="regime">
|
||||||
|
<option value="">Tous</option>
|
||||||
|
{% for v,l in regimes %}<option value="{{ v }}" {% if filtres.regime == v %}selected{% endif %}>{{ l }}</option>{% endfor %}
|
||||||
|
</select></label>
|
||||||
|
<label>Du<input type="date" name="du" value="{{ filtres.du }}"></label>
|
||||||
|
<label>Au<input type="date" name="au" value="{{ filtres.au }}"></label>
|
||||||
|
<label>Trier<select name="tri">
|
||||||
|
<option value="date" {% if filtres.tri == 'date' %}selected{% endif %}>Date</option>
|
||||||
|
<option value="montant" {% if filtres.tri == 'montant' %}selected{% endif %}>Montant</option>
|
||||||
|
<option value="libelle" {% if filtres.tri == 'libelle' %}selected{% endif %}>Libellé</option>
|
||||||
|
</select></label>
|
||||||
|
<label>Sens<select name="sens">
|
||||||
|
<option value="desc" {% if filtres.sens == 'desc' %}selected{% endif %}>↓ décroissant</option>
|
||||||
|
<option value="asc" {% if filtres.sens == 'asc' %}selected{% endif %}>↑ croissant</option>
|
||||||
|
</select></label>
|
||||||
|
<button type="submit" class="btn-primary">Filtrer</button>
|
||||||
|
<a class="btn-mini" href="/gestion/?entreprise={{ entreprise.pk }}" style="padding:9px 12px;border:1px solid var(--line);border-radius:9px;">Réinitialiser</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if entreprise %}
|
||||||
|
{% if rows %}
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="gtable">
|
||||||
|
<thead><tr>
|
||||||
|
<th class="chkcol"><input type="checkbox" id="chkall" title="Tout sélectionner"></th>
|
||||||
|
<th>Date</th><th>Type</th><th>Libellé</th><th>Catégorie</th>
|
||||||
|
<th>Montant</th><th>TVA</th><th></th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in rows %}
|
||||||
|
<tr>
|
||||||
|
<td class="chkcol"><input type="checkbox" class="rowchk" name="sel" value="{{ r.type }}:{{ r.id }}" form="bulkform"></td>
|
||||||
|
<td>{{ r.date|date:"d/m/Y" }}</td>
|
||||||
|
<td><span class="tag {{ r.type }}">{{ r.type_label }}</span></td>
|
||||||
|
<td>{{ r.libelle }}</td>
|
||||||
|
<td>{{ r.categorie }}{% if r.neutre %}<span class="neutre-badge">neutre</span>{% endif %}</td>
|
||||||
|
<td class="{% if r.montant >= 0 %}pos{% else %}neg{% endif %}">{{ r.montant|floatformat:2|unlocalize }} €</td>
|
||||||
|
<td>{% if r.tva %}{{ r.tva|floatformat:2|unlocalize }}{% else %}—{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn-mini"
|
||||||
|
data-edit
|
||||||
|
data-type="{{ r.type }}" data-id="{{ r.id }}"
|
||||||
|
data-libelle="{{ r.libelle }}"
|
||||||
|
data-date="{{ r.date|date:'Y-m-d' }}"
|
||||||
|
data-montant="{{ r.montant|floatformat:2|unlocalize }}"
|
||||||
|
data-cat="{{ r.categorie_id }}"
|
||||||
|
data-activite="{{ r.activite }}"
|
||||||
|
data-regime="{{ r.regime }}"
|
||||||
|
data-taux="{{ r.taux|unlocalize }}"
|
||||||
|
data-statut="{{ r.statut }}"
|
||||||
|
data-sens="{{ r.sens }}"
|
||||||
|
data-encaissement="{{ r.date_encaissement|date:'Y-m-d' }}"
|
||||||
|
data-emission="{{ r.date_emission|date:'Y-m-d' }}"
|
||||||
|
data-editmontant="{{ r.editable_montant|yesno:'1,0' }}"
|
||||||
|
>Modifier</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="bulkbar" id="bulkform" method="post" action="/gestion/"
|
||||||
|
onsubmit="return confirm('Supprimer définitivement les opérations sélectionnées ?');">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="delete_bulk">
|
||||||
|
<input type="hidden" name="entreprise" value="{{ entreprise.pk }}">
|
||||||
|
<input type="hidden" name="retour_qs" id="bulk_retour">
|
||||||
|
<span class="count"><span id="bulkcount">0</span> sélectionnée(s)</span>
|
||||||
|
<button type="submit" class="btn-danger">Supprimer la sélection</button>
|
||||||
|
<button type="button" onclick="clearSel()" style="background:var(--card);border:1px solid var(--line);">Annuler</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty">Aucune opération ne correspond à ces filtres.</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Modal d'édition -->
|
||||||
|
<div class="overlay" id="ov">
|
||||||
|
<div class="modal">
|
||||||
|
<h3 id="mtitre">Modifier l'opération</h3>
|
||||||
|
<form method="post" action="/gestion/" id="mform">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="edit">
|
||||||
|
<input type="hidden" name="entreprise" value="{{ entreprise.pk }}">
|
||||||
|
<input type="hidden" name="type" id="f_type">
|
||||||
|
<input type="hidden" name="id" id="f_id">
|
||||||
|
<input type="hidden" name="retour_qs" id="f_retour">
|
||||||
|
|
||||||
|
<div class="field"><label>Date</label><input type="date" name="date" id="f_date"></div>
|
||||||
|
<div class="field"><label>Libellé</label><input type="text" name="libelle" id="f_libelle"></div>
|
||||||
|
|
||||||
|
<div class="field" id="g_cat">
|
||||||
|
<label>Catégorie</label>
|
||||||
|
<select name="categorie" id="f_cat">
|
||||||
|
<option value="">(à catégoriser)</option>
|
||||||
|
{% for c in categories %}<option value="{{ c.pk }}">{{ c.nom }}{% if c.exclure_calculs %} · neutre{% endif %}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="explic" id="f_explic">Choisissez une catégorie pour voir son explication.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field hidegroup" id="g_activite">
|
||||||
|
<label>Activité (facultatif)</label>
|
||||||
|
<select name="activite" id="f_activite">
|
||||||
|
<option value="">—</option>
|
||||||
|
{% for v,l in activites %}<option value="{{ v }}">{{ l }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field hidegroup" id="g_regime">
|
||||||
|
<label>Régime de TVA</label>
|
||||||
|
<select name="regime" id="f_regime">
|
||||||
|
{% for v,l in regimes %}<option value="{{ v }}">{{ l }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field hidegroup" id="g_taux">
|
||||||
|
<label>Taux de TVA</label>
|
||||||
|
<select name="taux" id="f_taux">
|
||||||
|
{% for v,l in taux_choices %}<option value="{{ v|unlocalize }}">{{ l }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field hidegroup" id="g_sens">
|
||||||
|
<label>Sens</label>
|
||||||
|
<select name="sens" id="f_sens">
|
||||||
|
<option value="debit">Débit (sortie)</option>
|
||||||
|
<option value="credit">Crédit (entrée)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field hidegroup" id="g_statut">
|
||||||
|
<label>Statut</label>
|
||||||
|
<select name="statut" id="f_statut">
|
||||||
|
{% for v,l in statuts %}<option value="{{ v }}">{{ l }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field hidegroup" id="g_encaissement">
|
||||||
|
<label>Date d'encaissement (compte le CA)</label>
|
||||||
|
<input type="date" name="date_encaissement" id="f_encaissement">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field"><label id="lbl_montant">Montant (€)</label>
|
||||||
|
<input type="number" step="0.01" name="montant" id="f_montant"></div>
|
||||||
|
<div class="explic hidegroup" id="note_montant">Le montant est géré par les lignes de facture — modifiez-le dans l'admin.</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="btn-danger" onclick="supprimer()">Supprimer</button>
|
||||||
|
<div>
|
||||||
|
<button type="button" onclick="fermer()" style="margin-right:6px;">Annuler</button>
|
||||||
|
<button type="submit" class="btn-primary">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/gestion/" id="delform" style="display:none;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="entreprise" value="{{ entreprise.pk }}">
|
||||||
|
<input type="hidden" name="type" id="d_type">
|
||||||
|
<input type="hidden" name="id" id="d_id">
|
||||||
|
<input type="hidden" name="retour_qs" id="d_retour">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ cats_json|json_script:"cats-data" }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
const CATS = JSON.parse(document.getElementById('cats-data').textContent);
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
function retourQs(){
|
||||||
|
const p = new URLSearchParams(window.location.search);
|
||||||
|
p.delete('entreprise');
|
||||||
|
return p.toString();
|
||||||
|
}
|
||||||
|
function show(id, on){ $(id).classList.toggle('hidegroup', !on); }
|
||||||
|
|
||||||
|
function majExplic(){
|
||||||
|
const c = CATS[$('f_cat').value];
|
||||||
|
const box = $('f_explic');
|
||||||
|
if(!c){ box.innerHTML = 'Non catégorisé : le calcul fiscal peut être incomplet.'; return; }
|
||||||
|
const badge = c.neutre ? ' <b>(neutre)</b>' : '';
|
||||||
|
const compte = c.compte ? ' · compte ' + c.compte : '';
|
||||||
|
box.innerHTML = '<b>' + c.nom + '</b>' + badge + compte + '<br>' + c.explication;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ouvrir(btn){
|
||||||
|
const d = btn.dataset, t = d.type;
|
||||||
|
$('f_type').value = t; $('f_id').value = d.id;
|
||||||
|
$('d_type').value = t; $('d_id').value = d.id;
|
||||||
|
const qs = retourQs();
|
||||||
|
$('f_retour').value = qs; $('d_retour').value = qs;
|
||||||
|
$('f_date').value = d.date || '';
|
||||||
|
$('f_libelle').value = d.libelle || '';
|
||||||
|
$('f_montant').value = Math.abs(parseFloat(d.montant || '0')).toFixed(2);
|
||||||
|
$('f_cat').value = d.cat || '';
|
||||||
|
$('f_activite').value = d.activite || '';
|
||||||
|
$('f_regime').value = d.regime || 'nationale';
|
||||||
|
$('f_taux').value = (parseFloat(d.taux || '0') || 0).toString();
|
||||||
|
$('f_sens').value = d.sens || 'debit';
|
||||||
|
$('f_statut').value = d.statut || '';
|
||||||
|
$('f_encaissement').value = d.encaissement || '';
|
||||||
|
|
||||||
|
const dep = t === 'depense', ope = t === 'operation', rec = t === 'recette';
|
||||||
|
show('g_cat', dep || ope);
|
||||||
|
show('g_activite', dep);
|
||||||
|
show('g_regime', dep);
|
||||||
|
show('g_taux', dep || rec);
|
||||||
|
show('g_sens', ope);
|
||||||
|
show('g_statut', rec);
|
||||||
|
show('g_encaissement', rec);
|
||||||
|
const editMontant = d.editmontant === '1';
|
||||||
|
$('f_montant').readOnly = !editMontant;
|
||||||
|
show('note_montant', !editMontant);
|
||||||
|
$('lbl_montant').textContent = dep ? 'Montant payé — TTC (€)' : 'Montant (€)';
|
||||||
|
$('mtitre').textContent = 'Modifier — ' + (rec ? 'recette' : dep ? 'dépense' : 'opération bancaire');
|
||||||
|
majExplic();
|
||||||
|
$('ov').classList.add('on');
|
||||||
|
}
|
||||||
|
function fermer(){ $('ov').classList.remove('on'); }
|
||||||
|
function supprimer(){
|
||||||
|
if(confirm('Supprimer définitivement cette opération ?')) $('delform').submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sélection multiple */
|
||||||
|
function majBulk(){
|
||||||
|
const checked = document.querySelectorAll('.rowchk:checked').length;
|
||||||
|
$('bulkcount').textContent = checked;
|
||||||
|
document.getElementById('bulkform').classList.toggle('on', checked > 0);
|
||||||
|
const all = $('chkall'); const total = document.querySelectorAll('.rowchk').length;
|
||||||
|
if(all){ all.checked = checked === total && total > 0; all.indeterminate = checked > 0 && checked < total; }
|
||||||
|
}
|
||||||
|
function clearSel(){
|
||||||
|
document.querySelectorAll('.rowchk').forEach(c => c.checked = false);
|
||||||
|
if($('chkall')) $('chkall').checked = false;
|
||||||
|
majBulk();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const b = e.target.closest('[data-edit]');
|
||||||
|
if(b) ouvrir(b);
|
||||||
|
});
|
||||||
|
$('f_cat').addEventListener('change', majExplic);
|
||||||
|
$('ov').addEventListener('click', e => { if(e.target === $('ov')) fermer(); });
|
||||||
|
document.addEventListener('keydown', e => { if(e.key === 'Escape') fermer(); });
|
||||||
|
|
||||||
|
const chkall = $('chkall');
|
||||||
|
if(chkall){
|
||||||
|
chkall.addEventListener('change', () => {
|
||||||
|
document.querySelectorAll('.rowchk').forEach(c => c.checked = chkall.checked);
|
||||||
|
majBulk();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.rowchk').forEach(c => c.addEventListener('change', majBulk));
|
||||||
|
const br = $('bulk_retour'); if(br) br.value = retourQs();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,49 +1,38 @@
|
|||||||
{% load l10n %}<!DOCTYPE html>
|
{% extends "compta/base.html" %}
|
||||||
<html lang="fr">
|
{% load l10n %}
|
||||||
<head>
|
{% block titre %}Importer{% endblock %}
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
{% block extrastyle %}
|
||||||
<title>JB Compta — Import</title>
|
|
||||||
<style>
|
<style>
|
||||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
.ic label { display:block; font-size:12px; color:var(--muted); margin:10px 0 4px; }
|
||||||
--accent:#38bdf8; --ok:#34d399; --bad:#f87171; }
|
.ic input, .ic select, .ic textarea { width:100%; background:var(--panel); }
|
||||||
* { box-sizing:border-box; }
|
.ic textarea { min-height:90px; font-family:ui-monospace,monospace; }
|
||||||
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
|
.ic .card { margin-bottom:16px; }
|
||||||
background:var(--bg); color:var(--txt); padding:24px; }
|
.ic .card.indy { border-color:#0e7490; }
|
||||||
h1 { margin:0 0 4px; font-size:22px; } h2{font-size:15px;color:var(--muted);margin:0 0 8px;}
|
|
||||||
a { color:var(--accent); }
|
|
||||||
.sub { color:var(--muted); margin-bottom:14px; }
|
|
||||||
.card { background:var(--card); border:1px solid #334155; border-radius:12px; padding:16px; margin-bottom:16px; }
|
|
||||||
.card.indy { border-color:#0e7490; }
|
|
||||||
label { display:block; font-size:12px; color:var(--muted); margin:8px 0 4px; }
|
|
||||||
input, select, textarea { background:#0f172a; color:var(--txt); border:1px solid #334155;
|
|
||||||
border-radius:8px; padding:8px 10px; font-size:14px; width:100%; }
|
|
||||||
textarea { min-height:90px; font-family:ui-monospace,monospace; }
|
|
||||||
.grid3 { display:grid; grid-template-columns:2fr 1fr 1fr; gap:12px; }
|
.grid3 { display:grid; grid-template-columns:2fr 1fr 1fr; gap:12px; }
|
||||||
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||||||
@media(max-width:700px){ .grid3,.grid2{grid-template-columns:1fr;} }
|
@media(max-width:700px){ .grid3,.grid2{grid-template-columns:1fr;} }
|
||||||
button { cursor:pointer; border-radius:8px; border:1px solid #334155; background:var(--card);
|
.ic .actions { display:flex; gap:12px; margin-top:14px; flex-wrap:wrap; }
|
||||||
color:var(--txt); padding:9px 16px; font-size:14px; }
|
.ic table { font-size:13px; }
|
||||||
button.primary { background:var(--accent); color:#04263a; border:none; font-weight:600; }
|
.ic th, .ic td { padding:5px 7px; text-align:left; }
|
||||||
.actions { display:flex; gap:12px; margin-top:12px; }
|
.ic td input, .ic td select { padding:6px 8px; }
|
||||||
table { width:100%; border-collapse:collapse; font-size:13px; }
|
.ic .num { text-align:right; }
|
||||||
th,td { padding:4px 6px; border-bottom:1px solid #334155; text-align:left; }
|
code { background:var(--panel); padding:1px 6px; border-radius:5px; }
|
||||||
td input, td select { padding:5px 7px; }
|
|
||||||
.num { text-align:right; }
|
|
||||||
code { background:#0f172a; padding:1px 6px; border-radius:5px; }
|
|
||||||
.msg { padding:10px 14px; border-radius:8px; margin-bottom:8px; }
|
|
||||||
.msg.bad { background:#7f1d1d; } .msg.ok { background:#064e3b; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
{% endblock %}
|
||||||
<body>
|
|
||||||
<h1>Importer un relevé</h1>
|
|
||||||
<div class="sub"><a href="/dashboard/">← Tableau de bord</a> · <a href="/saisie/">Saisie manuelle</a></div>
|
|
||||||
|
|
||||||
|
{% block topbar %}
|
||||||
|
<h1>Importer un relevé</h1>
|
||||||
|
<div class="sub">Synchronisation Indy, CSV, ou capture d'écran (OCR) — avec aperçu éditable et anti-doublon.</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="ic">
|
||||||
{% if erreurs %}{% for e in erreurs %}<div class="msg bad">{{ e }}</div>{% endfor %}{% endif %}
|
{% if erreurs %}{% for e in erreurs %}<div class="msg bad">{{ e }}</div>{% endfor %}{% endif %}
|
||||||
{% if resultat %}
|
{% if resultat %}
|
||||||
<div class="msg ok">Import terminé :
|
<div class="msg">Import terminé :
|
||||||
{% for k, v in resultat.items %}{{ v }} {{ k }}{% if not forloop.last %} · {% endif %}{% endfor %}.
|
{% for k, v in resultat.items %}{{ v }} {{ k }}{% if not forloop.last %} · {% endif %}{% endfor %}.
|
||||||
Voir <a href="/admin/compta/depense/">dépenses</a> · <a href="/admin/compta/facture/">factures</a>.</div>
|
Voir <a href="/gestion/">Entrées / sorties</a>.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if ocr_debug %}
|
{% if ocr_debug %}
|
||||||
@@ -60,7 +49,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h2>① Synchroniser depuis Indy (recommandé)</h2>
|
<h2>① Synchroniser depuis Indy (recommandé)</h2>
|
||||||
<div class="sub">Récupère tes transactions directement depuis l'API Indy. Anti-doublon automatique.
|
<div class="sub">Récupère tes transactions directement depuis l'API Indy. Anti-doublon automatique.
|
||||||
· <a href="/import/bookmarklet/">📌 Installer le favori « Indy → CSV »</a> (le plus simple, depuis ton navigateur)</div>
|
· <a href="/import/bookmarklet/">Installer le favori « Indy → CSV »</a> (le plus simple, depuis ton navigateur)</div>
|
||||||
<div class="grid3">
|
<div class="grid3">
|
||||||
<div>
|
<div>
|
||||||
<label>Entreprise</label>
|
<label>Entreprise</label>
|
||||||
@@ -74,7 +63,7 @@
|
|||||||
<label>Token Indy (Bearer, sans « Bearer ») — mémorisé sur l'entreprise, expire ~1 h</label>
|
<label>Token Indy (Bearer, sans « Bearer ») — mémorisé sur l'entreprise, expire ~1 h</label>
|
||||||
<textarea name="indy_token" placeholder="eyJ..."></textarea>
|
<textarea name="indy_token" placeholder="eyJ..."></textarea>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" name="action" value="indy" class="primary">Récupérer depuis Indy</button>
|
<button type="submit" name="action" value="indy" class="btn-primary">Récupérer depuis Indy</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -98,10 +87,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<label>…ou fichier CSV</label>
|
<label>…ou fichier CSV</label>
|
||||||
<input type="file" name="fichier" accept=".csv,.txt">
|
<input type="file" name="fichier" accept=".csv,.txt">
|
||||||
<label>…ou colle des lignes CSV <span class="sub">(date ; libelle ; categorie ; activite ; taux_tva ; regime ; montant)</span></label>
|
<label>…ou colle des lignes CSV <span class="muted">(date ; libelle ; categorie ; activite ; taux_tva ; regime ; montant)</span></label>
|
||||||
<textarea name="texte" placeholder="2026-06-25;ALTISSIMO;Vente de produits finis;vente;20;;168.40">{{ texte }}</textarea>
|
<textarea name="texte" placeholder="2026-06-25;ALTISSIMO;Vente de produits finis;vente;20;;168.40">{{ texte }}</textarea>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" name="action" value="preview" class="primary">Analyser / Aperçu</button>
|
<button type="submit" name="action" value="preview" class="btn-primary">Analyser / Aperçu</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -111,6 +100,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="entreprise" value="{{ entreprise_id }}">
|
<input type="hidden" name="entreprise" value="{{ entreprise_id }}">
|
||||||
<h2>Aperçu éditable — {{ lignes|length }} ligne(s){% if source == 'indy' %} (Indy){% elif source == 'ocr' %} (OCR){% endif %}</h2>
|
<h2>Aperçu éditable — {{ lignes|length }} ligne(s){% if source == 'indy' %} (Indy){% elif source == 'ocr' %} (OCR){% endif %}</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Date</th><th>Libellé</th><th>Catégorie</th><th>Régime TVA</th><th>Taux %</th><th>Montant €</th><th>Type</th></tr></thead>
|
<thead><tr><th>Date</th><th>Libellé</th><th>Catégorie</th><th>Régime TVA</th><th>Taux %</th><th>Montant €</th><th>Type</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -138,14 +128,18 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" name="action" value="save" class="primary">Enregistrer ces opérations</button>
|
<button type="submit" name="action" value="save" class="btn-primary">Enregistrer ces opérations</button>
|
||||||
<a href="/import/"><button type="button">Recommencer</button></a>
|
<a href="/import/"><button type="button">Recommencer</button></a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<script>
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
window.addEventListener('paste', function(e){
|
window.addEventListener('paste', function(e){
|
||||||
var input = document.getElementById('imgInput');
|
var input = document.getElementById('imgInput');
|
||||||
if(!input || !e.clipboardData) return;
|
if(!input || !e.clipboardData) return;
|
||||||
@@ -159,6 +153,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
{% endblock %}
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -1,56 +1,37 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "compta/base.html" %}
|
||||||
<html lang="fr">
|
{% block titre %}Saisir{% endblock %}
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
{% block extrastyle %}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>JB Compta — Saisie</title>
|
|
||||||
<style>
|
<style>
|
||||||
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
|
.sp label { display:block; font-size:12px; color:var(--muted); margin-bottom:4px; }
|
||||||
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
|
.sp input, .sp select { width:100%; background:var(--panel); }
|
||||||
* { box-sizing:border-box; }
|
.sp .card { margin-bottom:16px; }
|
||||||
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
|
.sp .head { display:grid; grid-template-columns:1fr 1fr 2fr 1fr; gap:12px; }
|
||||||
background:var(--bg); color:var(--txt); padding:24px; }
|
|
||||||
h1 { margin:0 0 4px; font-size:22px; }
|
|
||||||
.sub { color:var(--muted); margin-bottom:20px; }
|
|
||||||
a { color:var(--accent); }
|
|
||||||
.card { background:var(--card); border:1px solid #334155; border-radius:12px;
|
|
||||||
padding:16px; margin-bottom:16px; }
|
|
||||||
label { display:block; font-size:12px; color:var(--muted); margin-bottom:4px; }
|
|
||||||
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; }
|
|
||||||
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||||||
@media(max-width:700px){ .head,.grid2{grid-template-columns:1fr;} }
|
@media(max-width:700px){ .sp .head,.grid2{grid-template-columns:1fr;} }
|
||||||
table { width:100%; border-collapse:collapse; font-size:13px; margin-top:8px; }
|
.sp table { font-size:13px; margin-top:8px; }
|
||||||
th,td { padding:6px; border-bottom:1px solid #334155; text-align:left; vertical-align:top; }
|
.sp th, .sp td { padding:6px; text-align:left; vertical-align:top; }
|
||||||
th { color:var(--muted); font-weight:600; }
|
.sp td input, .sp td select { padding:6px 8px; }
|
||||||
td input, td select { padding:6px 8px; }
|
.sp .num { text-align:right; }
|
||||||
.num { text-align:right; }
|
button.mini { padding:4px 10px; }
|
||||||
button { cursor:pointer; border-radius:8px; border:1px solid #334155;
|
|
||||||
background:var(--card); color:var(--txt); padding:8px 14px; font-size:14px; }
|
|
||||||
button.primary { background:var(--accent); color:#04263a; border:none; font-weight:600; }
|
|
||||||
button.mini { padding:4px 8px; }
|
|
||||||
.totaux { display:flex; gap:24px; align-items:center; margin-top:12px; flex-wrap:wrap; }
|
.totaux { display:flex; gap:24px; align-items:center; margin-top:12px; flex-wrap:wrap; }
|
||||||
.totaux b { font-size:18px; }
|
.totaux b { font-size:18px; }
|
||||||
.ok{color:var(--ok)} .bad{color:var(--bad)}
|
.sp .actions { display:flex; gap:12px; margin-top:16px; }
|
||||||
.msg { padding:10px 14px; border-radius:8px; margin-bottom:10px; }
|
|
||||||
.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 { display:inline-block; padding:2px 10px; border-radius:99px; font-size:12px; }
|
||||||
.badge.dep { background:#7f1d1d; }
|
.badge.dep { background:#7f1d1d; } .badge.ven { background:#064e3b; }
|
||||||
.badge.ven { background:#064e3b; }
|
|
||||||
.hidden { display:none; }
|
.hidden { display:none; }
|
||||||
.hint { color:var(--muted); font-size:12px; margin-top:6px; }
|
.hint { color:var(--muted); font-size:12px; margin-top:6px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
{% endblock %}
|
||||||
<body>
|
|
||||||
|
{% block topbar %}
|
||||||
<h1>Saisie d'une opération</h1>
|
<h1>Saisie d'une opération</h1>
|
||||||
<div class="sub">Montant <b>négatif</b> = dépense · <b>positif</b> = vente/encaissement.
|
<div class="sub">Montant <b>négatif</b> = dépense · <b>positif</b> = vente / encaissement.</div>
|
||||||
· <a href="/dashboard/">← Tableau de bord</a> · <a href="/import/">Importer un relevé</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 %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="sp">
|
||||||
<form method="post" id="frm">
|
<form method="post" id="frm">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -77,9 +58,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== PANEL DÉPENSE (montant négatif) ===== -->
|
<!-- PANEL DÉPENSE (montant négatif) -->
|
||||||
<div class="card hidden" id="panel-depense">
|
<div class="card hidden" id="panel-depense">
|
||||||
<div class="sub" style="margin:0 0 8px">Dépense — éclate en une ou plusieurs lignes.</div>
|
<div class="sub" style="margin:0 0 8px">Dépense — éclate en une ou plusieurs lignes.</div>
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -94,6 +76,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="lignes"></tbody>
|
<tbody id="lignes"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
<div class="actions"><button type="button" onclick="addRow()">+ Ajouter une ligne</button></div>
|
<div class="actions"><button type="button" onclick="addRow()">+ Ajouter une ligne</button></div>
|
||||||
<div class="totaux">
|
<div class="totaux">
|
||||||
<span>Réparti : <b id="repartis">0.00</b> €</span>
|
<span>Réparti : <b id="repartis">0.00</b> €</span>
|
||||||
@@ -101,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== PANEL VENTE (montant positif) ===== -->
|
<!-- PANEL VENTE (montant positif) -->
|
||||||
<div class="card hidden" id="panel-vente">
|
<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="sub" style="margin:0 0 8px">Vente — crée une facture encaissée (ou un mouvement neutre).</div>
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
@@ -146,7 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" class="primary">Enregistrer</button>
|
<button type="submit" class="btn-primary">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -180,8 +163,11 @@
|
|||||||
<td><button type="button" class="mini" onclick="this.closest('tr').remove();recalc()">✕</button></td>
|
<td><button type="button" class="mini" onclick="this.closest('tr').remove();recalc()">✕</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<script>
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
function addRow(){
|
function addRow(){
|
||||||
document.getElementById('lignes').appendChild(
|
document.getElementById('lignes').appendChild(
|
||||||
document.getElementById('rowtpl').content.cloneNode(true));
|
document.getElementById('rowtpl').content.cloneNode(true));
|
||||||
@@ -209,6 +195,5 @@
|
|||||||
badge.className='badge ven'; badge.textContent='Vente'; }
|
badge.className='badge ven'; badge.textContent='Vente'; }
|
||||||
else { dep.classList.add('hidden'); ven.classList.add('hidden'); badge.textContent=''; }
|
else { dep.classList.add('hidden'); ven.classList.add('hidden'); badge.textContent=''; }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
{% endblock %}
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ from . import views
|
|||||||
from . import views_saisie
|
from . import views_saisie
|
||||||
from . import views_import
|
from . import views_import
|
||||||
from . import views_bookmarklet
|
from . import views_bookmarklet
|
||||||
|
from . import views_gestion
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("dashboard/", views.dashboard, name="dashboard"),
|
path("dashboard/", views.dashboard, name="dashboard"),
|
||||||
|
path("gestion/", views_gestion.gestion, name="gestion"),
|
||||||
path("dashboard/mensuel/", views.vue_mensuelle, name="vue_mensuelle"),
|
path("dashboard/mensuel/", views.vue_mensuelle, name="vue_mensuelle"),
|
||||||
path("declaration/urssaf/", views.declaration_urssaf, name="declaration_urssaf"),
|
path("declaration/urssaf/", views.declaration_urssaf, name="declaration_urssaf"),
|
||||||
path("declaration/tva/", views.declaration_tva, name="declaration_tva"),
|
path("declaration/tva/", views.declaration_tva, name="declaration_tva"),
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
"""
|
||||||
|
Page de gestion des entrées / sorties : vue unifiée de TOUTES les opérations
|
||||||
|
importées (recettes = factures, dépenses, opérations bancaires neutres).
|
||||||
|
|
||||||
|
- Filtrer (type, catégorie, régime, texte, dates), ordonner (date, montant, libellé).
|
||||||
|
- Modifier via une popup (avec explications sur les catégories) : la TVA est
|
||||||
|
recalculée automatiquement.
|
||||||
|
- Supprimer.
|
||||||
|
|
||||||
|
Tout est cloisonné par entreprise (Entreprise.accessibles).
|
||||||
|
"""
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from .models import (Entreprise, Facture, LigneFacture, Depense, Paiement,
|
||||||
|
Categorie, Activite, RegimeTva, StatutFacture, TAUX_TVA_CHOICES)
|
||||||
|
|
||||||
|
CENT = Decimal("0.01")
|
||||||
|
|
||||||
|
# Explications spécifiques (affichées dans la popup). Sinon, texte générique par usage.
|
||||||
|
EXPLIC = {
|
||||||
|
"vente": "Recette imposable : entre dans le chiffre d'affaires (CA) et dans "
|
||||||
|
"l'assiette des cotisations URSSAF. La TVA collectée est due.",
|
||||||
|
"depense": "Charge professionnelle : ne réduit PAS le CA micro, mais la TVA "
|
||||||
|
"payée est récupérable (TVA déductible) si vous êtes assujetti.",
|
||||||
|
"neutre": "Mouvement neutre : n'entre ni dans le CA, ni dans les cotisations, "
|
||||||
|
"ni dans la TVA. Pur mouvement de trésorerie (apport, prélèvement, "
|
||||||
|
"impôt, virement interne, remboursement…).",
|
||||||
|
}
|
||||||
|
EXPLIC_SPE = {
|
||||||
|
"Apport personnel": "Argent que vous injectez depuis vos fonds perso. Ce n'est "
|
||||||
|
"pas une recette : aucun impact fiscal.",
|
||||||
|
"Prélèvement personnel": "Argent que vous sortez pour vous. En micro, la "
|
||||||
|
"rémunération n'est pas une charge : mouvement neutre.",
|
||||||
|
"Remboursement reçu": "Remboursement d'un tiers (trop-perçu, avoir…). Ce n'est "
|
||||||
|
"pas un revenu d'activité : neutre.",
|
||||||
|
"Cotisation sociale Urssaf": "Vos cotisations URSSAF. Déjà prises en compte via "
|
||||||
|
"le taux URSSAF : ne pas les recompter en charge.",
|
||||||
|
"TVA payée": "Reversement de TVA à l'État. Neutre : c'est de la trésorerie, "
|
||||||
|
"pas une charge.",
|
||||||
|
"Remboursement de TVA": "Crédit de TVA remboursé par l'État. Neutre.",
|
||||||
|
"Virement interne": "Transfert entre vos propres comptes. Aucun impact.",
|
||||||
|
"CFE": "Cotisation Foncière des Entreprises. Impôt : suivi mais neutre côté TVA/CA.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _explication(cat):
|
||||||
|
if cat is None:
|
||||||
|
return ("Non catégorisé : pensez à choisir une catégorie pour un calcul "
|
||||||
|
"fiscal correct.")
|
||||||
|
return EXPLIC_SPE.get(cat.nom, EXPLIC.get(cat.usage, EXPLIC["depense"]))
|
||||||
|
|
||||||
|
|
||||||
|
def _entreprise(request, pk=None):
|
||||||
|
acc = Entreprise.accessibles(request.user)
|
||||||
|
pk = pk or request.GET.get("entreprise") or request.POST.get("entreprise")
|
||||||
|
if pk:
|
||||||
|
return acc.filter(pk=pk).first()
|
||||||
|
return acc.first()
|
||||||
|
|
||||||
|
|
||||||
|
def _dec(v, d="0"):
|
||||||
|
try:
|
||||||
|
return Decimal(str(v).replace(",", ".").strip() or d)
|
||||||
|
except (InvalidOperation, AttributeError):
|
||||||
|
return Decimal(d)
|
||||||
|
|
||||||
|
|
||||||
|
def _date(s):
|
||||||
|
for fmt in ("%Y-%m-%d", "%d/%m/%Y"):
|
||||||
|
try:
|
||||||
|
return datetime.strptime((s or "").strip(), fmt).date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _rows(entreprise):
|
||||||
|
"""Liste unifiée des opérations de l'entreprise (recettes, dépenses, neutres)."""
|
||||||
|
rows = []
|
||||||
|
for f in (Facture.objects.filter(entreprise=entreprise)
|
||||||
|
.prefetch_related("lignes").select_related("client")):
|
||||||
|
une_ligne = f.lignes.count() == 1
|
||||||
|
rows.append({
|
||||||
|
"type": "recette", "type_label": "Recette", "id": f.pk,
|
||||||
|
"date": f.date_encaissement or f.date_emission,
|
||||||
|
"libelle": str(f.client), "categorie": f"Facture {f.numero}",
|
||||||
|
"categorie_id": "", "activite": f.activite,
|
||||||
|
"regime": "nationale", "taux": f.lignes.first().taux_tva if une_ligne else "",
|
||||||
|
"montant": f.montant_ttc, "tva": f.montant_tva, "neutre": False,
|
||||||
|
"signe": Decimal("1"), "statut": f.statut,
|
||||||
|
"date_encaissement": f.date_encaissement, "date_emission": f.date_emission,
|
||||||
|
"editable_montant": une_ligne,
|
||||||
|
})
|
||||||
|
for d in Depense.objects.filter(entreprise=entreprise).select_related("categorie"):
|
||||||
|
rows.append({
|
||||||
|
"type": "depense", "type_label": "Dépense", "id": d.pk,
|
||||||
|
"date": d.date, "libelle": d.libelle,
|
||||||
|
"categorie": d.categorie.nom if d.categorie else "(à catégoriser)",
|
||||||
|
"categorie_id": d.categorie_id or "", "activite": d.activite,
|
||||||
|
"regime": d.regime_tva, "taux": d.taux_tva,
|
||||||
|
"montant": -d.montant_ttc, "tva": d.tva_deductible_value,
|
||||||
|
"neutre": d.neutre, "signe": Decimal("-1"), "editable_montant": True,
|
||||||
|
})
|
||||||
|
for p in Paiement.objects.filter(entreprise=entreprise).select_related("categorie"):
|
||||||
|
rows.append({
|
||||||
|
"type": "operation", "type_label": "Opération", "id": p.pk,
|
||||||
|
"date": p.date, "libelle": p.libelle,
|
||||||
|
"categorie": p.categorie.nom if p.categorie else "(non catégorisée)",
|
||||||
|
"categorie_id": p.categorie_id or "", "activite": "",
|
||||||
|
"regime": "", "taux": "",
|
||||||
|
"montant": p.montant_total if p.sens == "credit" else -p.montant_total,
|
||||||
|
"tva": Decimal("0"),
|
||||||
|
"neutre": bool(p.categorie and p.categorie.exclure_calculs),
|
||||||
|
"signe": Decimal("1") if p.sens == "credit" else Decimal("-1"),
|
||||||
|
"sens": p.sens, "editable_montant": True,
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _filtrer_trier(rows, request):
|
||||||
|
q = (request.GET.get("q") or "").strip().lower()
|
||||||
|
ftype = request.GET.get("type") or ""
|
||||||
|
fcat = request.GET.get("cat") or ""
|
||||||
|
fregime = request.GET.get("regime") or ""
|
||||||
|
dfrom = _date(request.GET.get("du"))
|
||||||
|
dto = _date(request.GET.get("au"))
|
||||||
|
tri = request.GET.get("tri") or "date"
|
||||||
|
sens = request.GET.get("sens") or "desc"
|
||||||
|
|
||||||
|
def garde(r):
|
||||||
|
if ftype and r["type"] != ftype:
|
||||||
|
return False
|
||||||
|
if fcat and str(r["categorie_id"]) != fcat:
|
||||||
|
return False
|
||||||
|
if fregime and r["regime"] != fregime:
|
||||||
|
return False
|
||||||
|
if q and q not in (r["libelle"] + " " + r["categorie"]).lower():
|
||||||
|
return False
|
||||||
|
if dfrom and r["date"] and r["date"] < dfrom:
|
||||||
|
return False
|
||||||
|
if dto and r["date"] and r["date"] > dto:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
rows = [r for r in rows if garde(r)]
|
||||||
|
cles = {"date": lambda r: (r["date"] or datetime(1900, 1, 1).date()),
|
||||||
|
"montant": lambda r: r["montant"],
|
||||||
|
"libelle": lambda r: r["libelle"].lower()}
|
||||||
|
rows.sort(key=cles.get(tri, cles["date"]), reverse=(sens == "desc"))
|
||||||
|
return rows, {"q": q, "type": ftype, "cat": fcat, "regime": fregime,
|
||||||
|
"du": request.GET.get("du") or "", "au": request.GET.get("au") or "",
|
||||||
|
"tri": tri, "sens": sens}
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def gestion(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
return _traiter_post(request)
|
||||||
|
|
||||||
|
entreprise = _entreprise(request)
|
||||||
|
if entreprise is None:
|
||||||
|
return render(request, "compta/gestion.html", {"entreprise": None})
|
||||||
|
|
||||||
|
rows, filtres = _filtrer_trier(_rows(entreprise), request)
|
||||||
|
total = sum((r["montant"] for r in rows), Decimal("0"))
|
||||||
|
entrees = sum((r["montant"] for r in rows if r["montant"] > 0), Decimal("0"))
|
||||||
|
sorties = sum((r["montant"] for r in rows if r["montant"] < 0), Decimal("0"))
|
||||||
|
|
||||||
|
cats = list(Categorie.objects.filter(actif=True))
|
||||||
|
cats_json = {str(c.pk): {"nom": c.nom, "usage": c.usage,
|
||||||
|
"neutre": c.exclure_calculs, "compte": c.compte_pcg,
|
||||||
|
"explication": _explication(c)} for c in cats}
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
"entreprise": entreprise,
|
||||||
|
"entreprises": Entreprise.accessibles(request.user),
|
||||||
|
"rows": rows, "filtres": filtres,
|
||||||
|
"total": total, "entrees": entrees, "sorties": sorties, "nb": len(rows),
|
||||||
|
"categories": cats, "cats_json": cats_json,
|
||||||
|
"activites": Activite.choices, "regimes": RegimeTva.choices,
|
||||||
|
"statuts": StatutFacture.choices, "taux_choices": TAUX_TVA_CHOICES,
|
||||||
|
"vue": "gestion",
|
||||||
|
}
|
||||||
|
return render(request, "compta/gestion.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def _retour(request, entreprise):
|
||||||
|
base = reverse("gestion")
|
||||||
|
qs = request.POST.get("retour_qs", "")
|
||||||
|
url = f"{base}?entreprise={entreprise.pk}"
|
||||||
|
if qs:
|
||||||
|
url += "&" + qs
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
def _traiter_post(request):
|
||||||
|
entreprise = _entreprise(request)
|
||||||
|
if entreprise is None:
|
||||||
|
return redirect(reverse("gestion"))
|
||||||
|
action = request.POST.get("action")
|
||||||
|
modeles = {"recette": Facture, "depense": Depense, "operation": Paiement}
|
||||||
|
|
||||||
|
if action == "delete_bulk":
|
||||||
|
n = 0
|
||||||
|
for token in request.POST.getlist("sel"):
|
||||||
|
t, _, pk = token.partition(":")
|
||||||
|
Mod = modeles.get(t)
|
||||||
|
if not Mod or not pk:
|
||||||
|
continue
|
||||||
|
obj = Mod.objects.filter(entreprise=entreprise, pk=pk).first()
|
||||||
|
if obj:
|
||||||
|
obj.delete()
|
||||||
|
n += 1
|
||||||
|
messages.success(request, f"{n} opération(s) supprimée(s).")
|
||||||
|
return _retour(request, entreprise)
|
||||||
|
|
||||||
|
typ = request.POST.get("type")
|
||||||
|
oid = request.POST.get("id")
|
||||||
|
Modele = modeles.get(typ)
|
||||||
|
if not Modele:
|
||||||
|
return _retour(request, entreprise)
|
||||||
|
obj = Modele.objects.filter(entreprise=entreprise, pk=oid).first()
|
||||||
|
if not obj:
|
||||||
|
messages.error(request, "Opération introuvable.")
|
||||||
|
return _retour(request, entreprise)
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
obj.delete()
|
||||||
|
messages.success(request, "Opération supprimée.")
|
||||||
|
return _retour(request, entreprise)
|
||||||
|
|
||||||
|
# action == "edit"
|
||||||
|
dt = _date(request.POST.get("date"))
|
||||||
|
libelle = (request.POST.get("libelle") or "").strip()
|
||||||
|
montant = _dec(request.POST.get("montant"))
|
||||||
|
cat_id = request.POST.get("categorie") or ""
|
||||||
|
cat = Categorie.objects.filter(pk=cat_id).first() if cat_id else None
|
||||||
|
|
||||||
|
if typ == "depense":
|
||||||
|
if dt:
|
||||||
|
obj.date = dt
|
||||||
|
obj.libelle = libelle or obj.libelle
|
||||||
|
obj.categorie = cat
|
||||||
|
obj.activite = request.POST.get("activite") or ""
|
||||||
|
obj.regime_tva = request.POST.get("regime") or RegimeTva.NATIONALE
|
||||||
|
obj.taux_tva = _dec(request.POST.get("taux"), "0")
|
||||||
|
obj.montant_ttc = abs(montant)
|
||||||
|
obj.save() # recalcule la TVA
|
||||||
|
elif typ == "operation":
|
||||||
|
if dt:
|
||||||
|
obj.date = dt
|
||||||
|
obj.libelle = libelle or obj.libelle
|
||||||
|
obj.categorie = cat
|
||||||
|
obj.sens = request.POST.get("sens") or obj.sens
|
||||||
|
obj.montant_total = abs(montant)
|
||||||
|
obj.save()
|
||||||
|
elif typ == "recette":
|
||||||
|
de = _date(request.POST.get("date_emission")) or _date(request.POST.get("date"))
|
||||||
|
if de:
|
||||||
|
obj.date_emission = de
|
||||||
|
obj.date_encaissement = _date(request.POST.get("date_encaissement"))
|
||||||
|
obj.statut = request.POST.get("statut") or obj.statut
|
||||||
|
ligne = obj.lignes.first()
|
||||||
|
if ligne and obj.lignes.count() == 1:
|
||||||
|
taux = _dec(request.POST.get("taux"), str(ligne.taux_tva))
|
||||||
|
ttc = abs(montant) if montant else obj.montant_ttc
|
||||||
|
ligne.prix_unitaire_ht = (ttc / (Decimal("1") + taux / Decimal("100"))).quantize(CENT)
|
||||||
|
ligne.taux_tva = taux
|
||||||
|
ligne.quantite = Decimal("1")
|
||||||
|
ligne.save()
|
||||||
|
obj.save()
|
||||||
|
obj.recompute_totaux()
|
||||||
|
|
||||||
|
messages.success(request, "Opération modifiée.")
|
||||||
|
return _retour(request, entreprise)
|
||||||
@@ -302,7 +302,7 @@ def import_csv(request):
|
|||||||
ctx = {"entreprises": entreprises,
|
ctx = {"entreprises": entreprises,
|
||||||
"categories": Categorie.objects.filter(actif=True),
|
"categories": Categorie.objects.filter(actif=True),
|
||||||
"activites": Activite.choices, "regimes": RegimeTva.choices,
|
"activites": Activite.choices, "regimes": RegimeTva.choices,
|
||||||
"annee_defaut": date_cls.today().year}
|
"annee_defaut": date_cls.today().year, "vue": "import"}
|
||||||
|
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
return render(request, "compta/import_csv.html", ctx)
|
return render(request, "compta/import_csv.html", ctx)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ def _context(entreprises, post=None):
|
|||||||
"categories_vente": Categorie.objects.filter(actif=True, usage__in=["vente", "neutre"]),
|
"categories_vente": Categorie.objects.filter(actif=True, usage__in=["vente", "neutre"]),
|
||||||
"today": date.today().isoformat(),
|
"today": date.today().isoformat(),
|
||||||
"post": post,
|
"post": post,
|
||||||
|
"vue": "saisie",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ CSRF_TRUSTED_ORIGINS = [
|
|||||||
o for o in os.getenv("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",") if o
|
o for o in os.getenv("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",") if o
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Derrière un reverse-proxy (Traefik, Nginx…) qui termine le TLS.
|
||||||
|
# Active-le avec DJANGO_BEHIND_PROXY=1 pour que Django reconnaisse le HTTPS
|
||||||
|
# transmis par le proxy (en-tête X-Forwarded-Proto) et sécurise les cookies.
|
||||||
|
if env_bool("DJANGO_BEHIND_PROXY", False):
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
# Redirige HTTP -> HTTPS seulement si demandé (laisser 0 si Traefik le fait déjà).
|
||||||
|
SECURE_SSL_REDIRECT = env_bool("DJANGO_SSL_REDIRECT", False)
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
|
|||||||
+2
-1
@@ -20,4 +20,5 @@ if [ "$SEED_DEMO" = "1" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "→ Démarrage de Gunicorn sur :8000"
|
echo "→ Démarrage de Gunicorn sur :8000"
|
||||||
exec gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 60
|
exec gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 60 \
|
||||||
|
--forwarded-allow-ips="*" --proxy-allow-from="*"
|
||||||
|
|||||||
Reference in New Issue
Block a user