Changes to be committed:

modified:   .env.example
	new file:   compta/__pycache__/__init__.cpython-310.pyc
	new file:   compta/__pycache__/admin.cpython-310.pyc
	new file:   compta/__pycache__/apps.cpython-310.pyc
	new file:   compta/__pycache__/models.cpython-310.pyc
	modified:   compta/admin.py
	new file:   compta/categories_indy.py
	modified:   compta/indy.py
	modified:   compta/management/commands/seed_reference.py
	new file:   compta/migrations/0010_categorie_compte_pcg.py
	modified:   compta/models.py
	modified:   compta/templates/compta/base.html
	new file:   compta/templates/compta/gestion.html
	modified:   compta/urls.py
	new file:   compta/views_gestion.py
	new file:   config/__pycache__/__init__.cpython-310.pyc
	new file:   config/__pycache__/settings.cpython-310.pyc
	modified:   config/settings.py
	modified:   entrypoint.sh
This commit is contained in:
2026-07-11 20:27:51 +02:00
parent 358cf35790
commit 0a4ba2001b
19 changed files with 743 additions and 63 deletions
+1
View File
@@ -53,6 +53,7 @@
<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="{% if vue == 'gestion' %}on{% endif %}" href="/gestion/?entreprise={{ entreprise.pk }}">Entrées / sorties</a>
<a class="saisie" href="/saisie/">+ Saisir</a>
<a href="/import/">Importer</a>
{% if user.is_staff %}<a href="/admin/">Admin</a>{% endif %}
+323
View File
@@ -0,0 +1,323 @@
{% load l10n %}<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JB Compta — Entrées / sorties</title>
<style>
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; --line:#334155; }
* { 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; }
a { color:var(--accent); text-decoration:none; }
.sub { color:var(--muted); margin-bottom:16px; }
.nav { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
.nav a, .nav button { padding:7px 13px; border-radius:8px; border:1px solid var(--line);
background:var(--card); font-size:14px; color:var(--txt); cursor:pointer; }
.nav a.on { background:var(--accent); color:#04263a; border-color:var(--accent); font-weight:600; }
select, button, input { background:var(--card); color:var(--txt); border:1px solid var(--line);
border-radius:8px; padding:8px 10px; font-size:14px; }
button { cursor:pointer; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; margin:8px 0 18px; }
.card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:14px; }
.card .label { color:var(--muted); font-size:12px; }
.card .value { font-size:20px; font-weight:600; margin-top:4px; }
.ok{color:var(--ok)} .warn{color:var(--warn)} .bad{color:var(--bad)}
form.filters { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:16px; align-items:end; }
form.filters label { display:flex; flex-direction:column; gap:3px; font-size:12px; color:var(--muted); }
table { width:100%; border-collapse:collapse; font-size:14px; }
td,th { padding:8px 10px; border-bottom:1px solid var(--line); text-align:left; }
th { color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.03em; }
td.num, th.num { text-align:right; font-variant-numeric:tabular-nums; }
tr:hover td { background:#243247; }
.tag { font-size:11px; padding:2px 7px; border-radius:99px; border:1px solid var(--line); }
.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:4px 9px; font-size:12px; }
.pos{color:var(--ok)} .neg{color:var(--bad)}
.msg { padding:10px 14px; border-radius:8px; margin-bottom:12px; background:#14532d33; border:1px solid #2f6b52; }
.msg.error { background:#7f1d1d33; border-color:#7a2f2f; }
/* modal */
.overlay { position:fixed; inset:0; background:#020617cc; display:none; align-items:center;
justify-content:center; padding:16px; z-index:50; }
.overlay.on { display:flex; }
.modal { background:var(--card); border:1px solid var(--line); border-radius:14px;
width:min(560px,100%); max-height:90vh; overflow:auto; padding:22px; }
.modal h3 { margin:0 0 14px; }
.modal .field { display:flex; flex-direction:column; gap:4px; margin-bottom:12px; }
.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:18px; }
.explic { background:#0b1220; border:1px solid var(--line); border-radius:10px;
padding:11px 13px; font-size:13px; color:var(--muted); margin-top:2px; }
.explic b { color:var(--txt); }
.hidegroup { display:none; }
.empty { color:var(--muted); padding:24px; text-align:center; }
</style>
</head>
<body>
{% if not entreprise %}
<h1>Aucune entreprise</h1>
<p class="sub">Crée une entreprise dans <a href="/admin/">l'administration</a>.</p>
{% else %}
<div class="nav">
<a href="/dashboard/?entreprise={{ entreprise.pk }}">Global</a>
<a href="/dashboard/mensuel/?entreprise={{ entreprise.pk }}">Mensuel</a>
<a href="/declaration/urssaf/?entreprise={{ entreprise.pk }}">Déclaration URSSAF</a>
<a href="/declaration/tva/?entreprise={{ entreprise.pk }}">Déclaration TVA</a>
<a class="on" href="/gestion/?entreprise={{ entreprise.pk }}">Entrées / sorties</a>
<a 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="margin:0 0 0 auto;">{% csrf_token %}<button type="submit">Déconnexion</button></form>
</div>
<h1>Entrées / sorties</h1>
<div class="sub">{{ entreprise.nom }} — vue unifiée de toutes les opérations importées</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">
<input type="hidden" name="entreprise" value="{{ entreprise.pk }}">
<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">Filtrer</button>
<a class="btn-mini" href="/gestion/?entreprise={{ entreprise.pk }}" style="padding:8px 12px;border:1px solid var(--line);border-radius:8px;">Réinitialiser</a>
</form>
{% if rows %}
<table>
<thead><tr>
<th>Date</th><th>Type</th><th>Libellé</th><th>Catégorie</th>
<th class="num">Montant</th><th class="num">TVA</th><th></th>
</tr></thead>
<tbody>
{% for r in rows %}
<tr>
<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="num {% if r.montant >= 0 %}pos{% else %}neg{% endif %}">{{ r.montant|floatformat:2|unlocalize }} €</td>
<td class="num">{% if r.tva %}{{ r.tva|floatformat:2|unlocalize }}{% else %}—{% endif %}</td>
<td class="num">
<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>
{% 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" onclick="supprimer()" style="border-color:#7a2f2f;color:var(--bad);">Supprimer</button>
<div>
<button type="button" onclick="fermer()" style="margin-right:6px;">Annuler</button>
<button type="submit" style="background:var(--accent);color:#04263a;border-color:var(--accent);font-weight:600;">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>
<script>
const CATS = {{ cats_json|safe }};
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 id = $('f_cat').value;
const c = CATS[id];
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;
const 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 || '';
// Champs par type
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();
}
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(); });
</script>
</body>
</html>