forked from seb_vallee/BillManager
Init project
This commit is contained in:
62
templates/auth/login.html
Normal file
62
templates/auth/login.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion — Facturation</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
body { display: flex; align-items: center; justify-content: center;
|
||||
min-height: 100vh; background: var(--bg); }
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 2.5rem 2rem;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
.login-card h1 { font-size: 1.4rem; color: var(--primary);
|
||||
margin-bottom: 0.25rem; text-align: center; }
|
||||
.login-card .subtitle { text-align: center; color: var(--muted);
|
||||
font-size: 0.85rem; margin-bottom: 2rem; }
|
||||
.erreur { background: #fee2e2; color: #991b1b; padding: 0.7em 1em;
|
||||
border-radius: 5px; font-size: 0.875rem; margin-bottom: 1rem; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 1rem; }
|
||||
label { font-size: 0.82rem; font-weight: 600; color: var(--muted);
|
||||
text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
input { padding: 0.6em 0.75em; border: 1px solid var(--border);
|
||||
border-radius: 5px; font-size: 0.95rem; width: 100%; }
|
||||
input:focus { outline: none; border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(52,152,219,0.15); }
|
||||
.btn-login { width: 100%; padding: 0.7em; background: var(--accent);
|
||||
color: white; border: none; border-radius: 5px;
|
||||
font-size: 1rem; cursor: pointer; margin-top: 0.5rem; }
|
||||
.btn-login:hover { background: #2980b9; }
|
||||
.logo { text-align: center; font-size: 2.5rem; margin-bottom: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="logo">🧾</div>
|
||||
<h1>Facturation</h1>
|
||||
<p class="subtitle">Connectez-vous pour continuer</p>
|
||||
|
||||
{% if erreur %}
|
||||
<div class="erreur">{{ erreur }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/login">
|
||||
<div class="form-group">
|
||||
<label>Nom d'utilisateur</label>
|
||||
<input type="text" name="username" autofocus autocomplete="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Mot de passe</label>
|
||||
<input type="password" name="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-login">Se connecter</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
40
templates/auth/user_form.html
Normal file
40
templates/auth/user_form.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ titre }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ titre }}</h1>
|
||||
<a href="/admin/utilisateurs" class="btn">← Retour</a>
|
||||
</div>
|
||||
|
||||
<form method="post" class="form-card">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Nom d'utilisateur *</label>
|
||||
<input type="text" name="username" required
|
||||
value="{{ user.username if user else '' }}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" name="email"
|
||||
value="{{ user.email if user else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Mot de passe {% if user %}(laisser vide = inchangé){% else %}*{% endif %}</label>
|
||||
<input type="password" name="password"
|
||||
{% if not user %}required{% endif %} autocomplete="new-password">
|
||||
</div>
|
||||
<div class="form-group" style="justify-content: flex-end; padding-top: 1.5rem;">
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
<input type="checkbox" name="is_admin" value="true"
|
||||
{% if user and user.is_admin %}checked{% endif %}
|
||||
style="width:auto;">
|
||||
Administrateur
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
<a href="/admin/utilisateurs" class="btn">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
57
templates/auth/utilisateurs.html
Normal file
57
templates/auth/utilisateurs.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Utilisateurs{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Utilisateurs</h1>
|
||||
<a href="/admin/utilisateurs/nouveau" class="btn btn-primary">+ Nouvel utilisateur</a>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom d'utilisateur</th>
|
||||
<th>Email</th>
|
||||
<th>Rôle</th>
|
||||
<th>Statut</th>
|
||||
<th>Créé le</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ u.username }}</strong>
|
||||
{% if u.id == current_user.id %}<em>(vous)</em>{% endif %}
|
||||
</td>
|
||||
<td>{{ u.email or "—" }}</td>
|
||||
<td>
|
||||
{% if u.is_admin %}
|
||||
<span class="badge badge-accepte">Admin</span>
|
||||
{% else %}
|
||||
<span class="badge badge-brouillon">Utilisateur</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if u.actif %}
|
||||
<span class="badge badge-payee">Actif</span>
|
||||
{% else %}
|
||||
<span class="badge badge-annulee">Inactif</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ u.created_at | date_fr }}</td>
|
||||
<td>
|
||||
<a href="/admin/utilisateurs/{{ u.id }}/modifier" class="btn btn-sm">Modifier</a>
|
||||
{% if u.id != current_user.id %}
|
||||
<form method="post" action="/admin/utilisateurs/{{ u.id }}/desactiver" style="display:inline">
|
||||
<button type="submit" class="btn btn-sm {% if u.actif %}btn-danger{% endif %}">
|
||||
{{ "Désactiver" if u.actif else "Réactiver" }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
33
templates/base.html
Normal file
33
templates/base.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Facturation{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<div class="nav-brand">🧾 Facturation</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/factures/">Factures</a></li>
|
||||
<li><a href="/devis/">Devis</a></li>
|
||||
<li><a href="/clients/">Clients</a></li>
|
||||
{% if current_user is defined and current_user %}
|
||||
{% if current_user.is_admin %}
|
||||
<li><a href="/admin/utilisateurs">Utilisateurs</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-user">{{ current_user.username }}</li>
|
||||
<li>
|
||||
<form method="post" action="/logout" style="display:inline">
|
||||
<button type="submit" class="btn-nav-logout">Déconnexion</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
45
templates/clients/form.html
Normal file
45
templates/clients/form.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ titre }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ titre }}</h1>
|
||||
<a href="/clients/" class="btn">← Retour</a>
|
||||
</div>
|
||||
|
||||
<form method="post" class="form-card">
|
||||
<div class="form-grid">
|
||||
<div class="form-group full">
|
||||
<label>Nom *</label>
|
||||
<input type="text" name="nom" required value="{{ client.nom if client else '' }}">
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>Adresse *</label>
|
||||
<input type="text" name="adresse" required value="{{ client.adresse if client else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Code postal *</label>
|
||||
<input type="text" name="code_postal" required value="{{ client.code_postal if client else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ville *</label>
|
||||
<input type="text" name="ville" required value="{{ client.ville if client else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" name="email" value="{{ client.email if client else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Téléphone</label>
|
||||
<input type="text" name="telephone" value="{{ client.telephone if client else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SIRET</label>
|
||||
<input type="text" name="siret" maxlength="14" value="{{ client.siret if client else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
<a href="/clients/" class="btn">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
41
templates/clients/liste.html
Normal file
41
templates/clients/liste.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Clients{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Clients</h1>
|
||||
<a href="/clients/nouveau" class="btn btn-primary">+ Nouveau client</a>
|
||||
</div>
|
||||
|
||||
{% if clients %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Ville</th>
|
||||
<th>Email</th>
|
||||
<th>SIRET</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in clients %}
|
||||
<tr>
|
||||
<td><strong>{{ c.nom }}</strong></td>
|
||||
<td>{{ c.code_postal }} {{ c.ville }}</td>
|
||||
<td>{{ c.email or "—" }}</td>
|
||||
<td>{{ c.siret or "—" }}</td>
|
||||
<td>
|
||||
<a href="/clients/{{ c.id }}/modifier" class="btn btn-sm">Modifier</a>
|
||||
<form method="post" action="/clients/{{ c.id }}/supprimer" style="display:inline">
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
onclick="return confirm('Supprimer ce client ?')">Supprimer</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty-state">Aucun client. <a href="/clients/nouveau">Créer le premier</a>.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
94
templates/devis/detail.html
Normal file
94
templates/devis/detail.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Devis {{ devis.numero }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Devis {{ devis.numero }}</h1>
|
||||
<div class="btn-group">
|
||||
<a href="/devis/" class="btn">← Retour</a>
|
||||
{% if devis.statut.value == 'brouillon' %}
|
||||
<a href="/devis/{{ devis.id }}/modifier" class="btn">Modifier</a>
|
||||
{% endif %}
|
||||
{% if devis.statut.value in ['envoye', 'accepte'] %}
|
||||
<form method="post" action="/devis/{{ devis.id }}/convertir" style="display:inline">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
onclick="return confirm('Convertir ce devis en facture ?')">
|
||||
→ Convertir en facture
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-meta">
|
||||
<div>
|
||||
<label>Client</label>
|
||||
<strong>{{ devis.client.nom }}</strong><br>
|
||||
{{ devis.client.adresse }}<br>
|
||||
{{ devis.client.code_postal }} {{ devis.client.ville }}
|
||||
</div>
|
||||
<div>
|
||||
<label>Dates</label>
|
||||
Émission : {{ devis.date_emission | date_fr }}<br>
|
||||
Validité : {{ devis.date_validite | date_fr }}
|
||||
</div>
|
||||
<div>
|
||||
<label>Statut</label>
|
||||
<span class="badge badge-{{ devis.statut.value }}">{{ devis.statut.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Qté</th>
|
||||
<th>PU HT</th>
|
||||
<th>Total HT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in devis.lignes %}
|
||||
<tr>
|
||||
<td>{{ l.description }}</td>
|
||||
<td>{{ l.quantite | int if l.quantite == l.quantite | int else l.quantite }}</td>
|
||||
<td>{{ l.prix_unitaire_ht | montant }}</td>
|
||||
<td>{{ l.total_ht | montant }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3"><strong>Total HT</strong></td>
|
||||
<td><strong>{{ devis.total_ht | montant }}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="tva-mention">TVA non applicable — art. 293B du CGI</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
{% if devis.notes %}
|
||||
<p><strong>Notes :</strong> {{ devis.notes }}</p>
|
||||
{% endif %}
|
||||
{% if devis.conditions %}
|
||||
<p><strong>Conditions :</strong> {{ devis.conditions }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
<h2>Changer le statut</h2>
|
||||
<form method="post" action="/devis/{{ devis.id }}/statut" style="display:flex; gap:1rem; align-items:flex-end;">
|
||||
<div class="form-group">
|
||||
<label>Nouveau statut</label>
|
||||
<select name="statut">
|
||||
<option value="brouillon" {% if devis.statut.value == 'brouillon' %}selected{% endif %}>Brouillon</option>
|
||||
<option value="envoye" {% if devis.statut.value == 'envoye' %}selected{% endif %}>Envoyé</option>
|
||||
<option value="accepte" {% if devis.statut.value == 'accepte' %}selected{% endif %}>Accepté</option>
|
||||
<option value="refuse" {% if devis.statut.value == 'refuse' %}selected{% endif %}>Refusé</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Mettre à jour</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
131
templates/devis/form.html
Normal file
131
templates/devis/form.html
Normal file
@@ -0,0 +1,131 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ titre }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ titre }}</h1>
|
||||
<a href="/devis/" class="btn">← Retour</a>
|
||||
</div>
|
||||
|
||||
<form method="post" class="form-card" id="form-devis">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Client *</label>
|
||||
<select name="client_id" required>
|
||||
<option value="">— Sélectionner —</option>
|
||||
{% for c in clients %}
|
||||
<option value="{{ c.id }}" {% if devis and devis.client_id == c.id %}selected{% endif %}>
|
||||
{{ c.nom }} — {{ c.ville }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Date d'émission *</label>
|
||||
<input type="date" name="date_emission" required value="{{ date_aujourd_hui }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Date de validité *</label>
|
||||
<input type="date" name="date_validite" required value="{{ date_validite_defaut }}">
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>Conditions</label>
|
||||
<textarea name="conditions" rows="2">{{ devis.conditions if devis else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>Notes internes</label>
|
||||
<textarea name="notes" rows="2">{{ devis.notes if devis else '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Lignes</h2>
|
||||
<table id="table-lignes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Qté</th>
|
||||
<th>PU HT (€)</th>
|
||||
<th>Total HT</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="lignes-body">
|
||||
{% if devis and devis.lignes %}
|
||||
{% for l in devis.lignes %}
|
||||
<tr class="ligne-row">
|
||||
<td><input type="text" class="ligne-desc" placeholder="Description" value="{{ l.description }}" required></td>
|
||||
<td><input type="number" class="ligne-qte" step="0.01" min="0.01" value="{{ l.quantite }}" required></td>
|
||||
<td><input type="number" class="ligne-pu" step="0.01" min="0" value="{{ l.prix_unitaire_ht }}" required></td>
|
||||
<td class="ligne-total">{{ (l.quantite * l.prix_unitaire_ht) | round(2) }} €</td>
|
||||
<td><button type="button" class="btn btn-sm btn-danger btn-suppr-ligne">✕</button></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" id="btn-ajouter-ligne" class="btn">+ Ajouter une ligne</button>
|
||||
|
||||
<div class="total-bloc">
|
||||
<strong>Total HT : <span id="total-ht">0,00 €</span></strong>
|
||||
<br><small>TVA non applicable — art. 293B du CGI</small>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="lignes_json" id="lignes_json">
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
<a href="/devis/" class="btn">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function recalculer() {
|
||||
let total = 0;
|
||||
document.querySelectorAll('.ligne-row').forEach(row => {
|
||||
const qte = parseFloat(row.querySelector('.ligne-qte').value) || 0;
|
||||
const pu = parseFloat(row.querySelector('.ligne-pu').value) || 0;
|
||||
const t = qte * pu;
|
||||
row.querySelector('.ligne-total').textContent = t.toFixed(2).replace('.', ',') + ' €';
|
||||
total += t;
|
||||
});
|
||||
document.getElementById('total-ht').textContent = total.toFixed(2).replace('.', ',') + ' €';
|
||||
}
|
||||
function ajouterLigne() {
|
||||
const tbody = document.getElementById('lignes-body');
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'ligne-row';
|
||||
tr.innerHTML = `
|
||||
<td><input type="text" class="ligne-desc" placeholder="Description" required></td>
|
||||
<td><input type="number" class="ligne-qte" step="0.01" min="0.01" value="1" required></td>
|
||||
<td><input type="number" class="ligne-pu" step="0.01" min="0" value="0" required></td>
|
||||
<td class="ligne-total">0,00 €</td>
|
||||
<td><button type="button" class="btn btn-sm btn-danger btn-suppr-ligne">✕</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
tr.querySelectorAll('input').forEach(i => i.addEventListener('input', recalculer));
|
||||
tr.querySelector('.btn-suppr-ligne').addEventListener('click', () => { tr.remove(); recalculer(); });
|
||||
}
|
||||
document.getElementById('btn-ajouter-ligne').addEventListener('click', ajouterLigne);
|
||||
document.querySelectorAll('.ligne-row').forEach(row => {
|
||||
row.querySelectorAll('input').forEach(i => i.addEventListener('input', recalculer));
|
||||
row.querySelector('.btn-suppr-ligne').addEventListener('click', () => { row.remove(); recalculer(); });
|
||||
});
|
||||
document.getElementById('form-devis').addEventListener('submit', function(e) {
|
||||
const lignes = [];
|
||||
let valide = true;
|
||||
document.querySelectorAll('.ligne-row').forEach(row => {
|
||||
const desc = row.querySelector('.ligne-desc').value.trim();
|
||||
const qte = parseFloat(row.querySelector('.ligne-qte').value);
|
||||
const pu = parseFloat(row.querySelector('.ligne-pu').value);
|
||||
if (!desc || isNaN(qte) || isNaN(pu)) { valide = false; return; }
|
||||
lignes.push({ description: desc, quantite: qte, prix_unitaire_ht: pu });
|
||||
});
|
||||
if (!valide || lignes.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Veuillez saisir au moins une ligne valide.');
|
||||
return;
|
||||
}
|
||||
document.getElementById('lignes_json').value = JSON.stringify(lignes);
|
||||
});
|
||||
recalculer();
|
||||
</script>
|
||||
{% endblock %}
|
||||
39
templates/devis/liste.html
Normal file
39
templates/devis/liste.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Devis{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Devis</h1>
|
||||
<a href="/devis/nouveau" class="btn btn-primary">+ Nouveau devis</a>
|
||||
</div>
|
||||
|
||||
{% if devis %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Numéro</th>
|
||||
<th>Date</th>
|
||||
<th>Client</th>
|
||||
<th>Montant HT</th>
|
||||
<th>Statut</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devis %}
|
||||
<tr>
|
||||
<td><strong>{{ d.numero }}</strong></td>
|
||||
<td>{{ d.date_emission | date_fr }}</td>
|
||||
<td>{{ d.client.nom }}</td>
|
||||
<td>{{ d.total_ht | montant }}</td>
|
||||
<td><span class="badge badge-{{ d.statut.value }}">{{ d.statut.value }}</span></td>
|
||||
<td>
|
||||
<a href="/devis/{{ d.id }}" class="btn btn-sm">Voir</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty-state">Aucun devis. <a href="/devis/nouveau">Créer le premier</a>.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
10
templates/erreur.html
Normal file
10
templates/erreur.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Erreur {{ status_code }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="form-card" style="text-align:center; padding: 3rem;">
|
||||
<p style="font-size: 3rem;">⚠️</p>
|
||||
<h1 style="margin: 1rem 0 0.5rem;">Erreur {{ status_code }}</h1>
|
||||
<p style="color: var(--muted);">{{ detail or "Une erreur est survenue." }}</p>
|
||||
<a href="/" class="btn btn-primary" style="margin-top: 1.5rem;">Retour à l'accueil</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
98
templates/factures/detail.html
Normal file
98
templates/factures/detail.html
Normal file
@@ -0,0 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Facture {{ facture.numero }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Facture {{ facture.numero }}</h1>
|
||||
<div class="btn-group">
|
||||
<a href="/factures/" class="btn">← Retour</a>
|
||||
{% if facture.statut.value == 'emise' %}
|
||||
<a href="/factures/{{ facture.id }}/modifier" class="btn">Modifier</a>
|
||||
{% endif %}
|
||||
<a href="/factures/{{ facture.id }}/apercu-pdf" class="btn" target="_blank">Aperçu PDF</a>
|
||||
<a href="/factures/{{ facture.id }}/pdf" class="btn btn-primary">⬇ Télécharger PDF</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-meta">
|
||||
<div>
|
||||
<label>Client</label>
|
||||
<strong>{{ facture.client.nom }}</strong><br>
|
||||
{{ facture.client.adresse }}<br>
|
||||
{{ facture.client.code_postal }} {{ facture.client.ville }}
|
||||
{% if facture.client.siret %}<br>SIRET : {{ facture.client.siret }}{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label>Dates</label>
|
||||
Émission : {{ facture.date_emission | date_fr }}<br>
|
||||
Échéance : {{ facture.date_echeance | date_fr }}<br>
|
||||
{% if facture.date_paiement %}
|
||||
Paiement : {{ facture.date_paiement | date_fr }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label>Statut</label>
|
||||
<span class="badge badge-{{ facture.statut.value }}">{{ facture.statut.value }}</span>
|
||||
{% if facture.devis_origine %}
|
||||
<br><small>Issu du devis {{ facture.devis_origine.numero }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Qté</th>
|
||||
<th>PU HT</th>
|
||||
<th>Total HT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in facture.lignes %}
|
||||
<tr>
|
||||
<td>{{ l.description }}</td>
|
||||
<td>{{ l.quantite | int if l.quantite == l.quantite | int else l.quantite }}</td>
|
||||
<td>{{ l.prix_unitaire_ht | montant }}</td>
|
||||
<td>{{ l.total_ht | montant }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3"><strong>Total HT</strong></td>
|
||||
<td><strong>{{ facture.total_ht | montant }}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="tva-mention">TVA non applicable — art. 293B du CGI</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
{% if facture.conditions_reglement %}
|
||||
<p><strong>Conditions de règlement :</strong> {{ facture.conditions_reglement }}</p>
|
||||
{% endif %}
|
||||
{% if facture.notes %}
|
||||
<p><strong>Notes :</strong> {{ facture.notes }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
<h2>Changer le statut</h2>
|
||||
<form method="post" action="/factures/{{ facture.id }}/statut" style="display:flex; gap:1rem; align-items:flex-end; flex-wrap:wrap;">
|
||||
<div class="form-group">
|
||||
<label>Nouveau statut</label>
|
||||
<select name="statut">
|
||||
<option value="emise" {% if facture.statut.value == 'emise' %}selected{% endif %}>Émise</option>
|
||||
<option value="payee" {% if facture.statut.value == 'payee' %}selected{% endif %}>Payée</option>
|
||||
<option value="annulee" {% if facture.statut.value == 'annulee' %}selected{% endif %}>Annulée</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="champ-date-paiement">
|
||||
<label>Date de paiement</label>
|
||||
<input type="date" name="date_paiement" value="{{ facture.date_paiement.isoformat() if facture.date_paiement else '' }}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Mettre à jour</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
139
templates/factures/form.html
Normal file
139
templates/factures/form.html
Normal file
@@ -0,0 +1,139 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ titre }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ titre }}</h1>
|
||||
<a href="/factures/" class="btn">← Retour</a>
|
||||
</div>
|
||||
|
||||
<form method="post" class="form-card" id="form-facture">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Client *</label>
|
||||
<select name="client_id" required>
|
||||
<option value="">— Sélectionner —</option>
|
||||
{% for c in clients %}
|
||||
<option value="{{ c.id }}" {% if facture and facture.client_id == c.id %}selected{% endif %}>
|
||||
{{ c.nom }} — {{ c.ville }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Date d'émission *</label>
|
||||
<input type="date" name="date_emission" required value="{{ date_aujourd_hui }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Date d'échéance *</label>
|
||||
<input type="date" name="date_echeance" required value="{{ date_echeance_defaut }}">
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>Conditions de règlement</label>
|
||||
<input type="text" name="conditions_reglement"
|
||||
value="{{ facture.conditions_reglement if facture else 'Paiement à réception de facture.' }}">
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>Notes internes</label>
|
||||
<textarea name="notes" rows="2">{{ facture.notes if facture else '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Lignes</h2>
|
||||
<div id="lignes-container">
|
||||
<table id="table-lignes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Qté</th>
|
||||
<th>PU HT (€)</th>
|
||||
<th>Total HT</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="lignes-body">
|
||||
{% if facture and facture.lignes %}
|
||||
{% for l in facture.lignes %}
|
||||
<tr class="ligne-row">
|
||||
<td><input type="text" class="ligne-desc" placeholder="Description" value="{{ l.description }}" required></td>
|
||||
<td><input type="number" class="ligne-qte" step="0.01" min="0.01" value="{{ l.quantite }}" required></td>
|
||||
<td><input type="number" class="ligne-pu" step="0.01" min="0" value="{{ l.prix_unitaire_ht }}" required></td>
|
||||
<td class="ligne-total">{{ (l.quantite * l.prix_unitaire_ht) | round(2) }} €</td>
|
||||
<td><button type="button" class="btn btn-sm btn-danger btn-suppr-ligne">✕</button></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" id="btn-ajouter-ligne" class="btn">+ Ajouter une ligne</button>
|
||||
</div>
|
||||
|
||||
<div class="total-bloc">
|
||||
<strong>Total HT : <span id="total-ht">0,00 €</span></strong>
|
||||
<br><small>TVA non applicable — art. 293B du CGI</small>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="lignes_json" id="lignes_json">
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
<a href="/factures/" class="btn">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function recalculer() {
|
||||
let total = 0;
|
||||
document.querySelectorAll('.ligne-row').forEach(row => {
|
||||
const qte = parseFloat(row.querySelector('.ligne-qte').value) || 0;
|
||||
const pu = parseFloat(row.querySelector('.ligne-pu').value) || 0;
|
||||
const t = qte * pu;
|
||||
row.querySelector('.ligne-total').textContent = t.toFixed(2).replace('.', ',') + ' €';
|
||||
total += t;
|
||||
});
|
||||
document.getElementById('total-ht').textContent = total.toFixed(2).replace('.', ',') + ' €';
|
||||
}
|
||||
|
||||
function ajouterLigne() {
|
||||
const tbody = document.getElementById('lignes-body');
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'ligne-row';
|
||||
tr.innerHTML = `
|
||||
<td><input type="text" class="ligne-desc" placeholder="Description" required></td>
|
||||
<td><input type="number" class="ligne-qte" step="0.01" min="0.01" value="1" required></td>
|
||||
<td><input type="number" class="ligne-pu" step="0.01" min="0" value="0" required></td>
|
||||
<td class="ligne-total">0,00 €</td>
|
||||
<td><button type="button" class="btn btn-sm btn-danger btn-suppr-ligne">✕</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
tr.querySelectorAll('input').forEach(i => i.addEventListener('input', recalculer));
|
||||
tr.querySelector('.btn-suppr-ligne').addEventListener('click', () => { tr.remove(); recalculer(); });
|
||||
}
|
||||
|
||||
document.getElementById('btn-ajouter-ligne').addEventListener('click', ajouterLigne);
|
||||
|
||||
document.querySelectorAll('.ligne-row').forEach(row => {
|
||||
row.querySelectorAll('input').forEach(i => i.addEventListener('input', recalculer));
|
||||
row.querySelector('.btn-suppr-ligne').addEventListener('click', () => { row.remove(); recalculer(); });
|
||||
});
|
||||
|
||||
document.getElementById('form-facture').addEventListener('submit', function(e) {
|
||||
const lignes = [];
|
||||
let valide = true;
|
||||
document.querySelectorAll('.ligne-row').forEach(row => {
|
||||
const desc = row.querySelector('.ligne-desc').value.trim();
|
||||
const qte = parseFloat(row.querySelector('.ligne-qte').value);
|
||||
const pu = parseFloat(row.querySelector('.ligne-pu').value);
|
||||
if (!desc || isNaN(qte) || isNaN(pu)) { valide = false; return; }
|
||||
lignes.push({ description: desc, quantite: qte, prix_unitaire_ht: pu });
|
||||
});
|
||||
if (!valide || lignes.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Veuillez saisir au moins une ligne valide.');
|
||||
return;
|
||||
}
|
||||
document.getElementById('lignes_json').value = JSON.stringify(lignes);
|
||||
});
|
||||
|
||||
recalculer();
|
||||
</script>
|
||||
{% endblock %}
|
||||
40
templates/factures/liste.html
Normal file
40
templates/factures/liste.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Factures{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Factures</h1>
|
||||
<a href="/factures/nouvelle" class="btn btn-primary">+ Nouvelle facture</a>
|
||||
</div>
|
||||
|
||||
{% if factures %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Numéro</th>
|
||||
<th>Date</th>
|
||||
<th>Client</th>
|
||||
<th>Montant HT</th>
|
||||
<th>Statut</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in factures %}
|
||||
<tr>
|
||||
<td><strong>{{ f.numero }}</strong></td>
|
||||
<td>{{ f.date_emission | date_fr }}</td>
|
||||
<td>{{ f.client.nom }}</td>
|
||||
<td>{{ f.total_ht | montant }}</td>
|
||||
<td><span class="badge badge-{{ f.statut.value }}">{{ f.statut.value }}</span></td>
|
||||
<td>
|
||||
<a href="/factures/{{ f.id }}" class="btn btn-sm">Voir</a>
|
||||
<a href="/factures/{{ f.id }}/pdf" class="btn btn-sm btn-primary">PDF</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty-state">Aucune facture. <a href="/factures/nouvelle">Créer la première</a>.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
297
templates/pdf/facture.html
Normal file
297
templates/pdf/facture.html
Normal file
@@ -0,0 +1,297 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 1.5cm 1.8cm;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'DejaVu Sans', Arial, sans-serif;
|
||||
font-size: 10pt;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2em;
|
||||
padding-bottom: 1.5em;
|
||||
border-bottom: 2px solid #2c3e50;
|
||||
}
|
||||
.emetteur h1 {
|
||||
font-size: 16pt;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.emetteur p { font-size: 9pt; color: #555; line-height: 1.6; }
|
||||
|
||||
.facture-titre {
|
||||
text-align: right;
|
||||
}
|
||||
.facture-titre h2 {
|
||||
font-size: 22pt;
|
||||
color: #2c3e50;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.facture-titre .numero {
|
||||
font-size: 13pt;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
.facture-titre .dates {
|
||||
font-size: 9pt;
|
||||
color: #555;
|
||||
margin-top: 0.5em;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.parties {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2em;
|
||||
gap: 2em;
|
||||
}
|
||||
.partie {
|
||||
flex: 1;
|
||||
padding: 1em;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.partie h3 {
|
||||
font-size: 8pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #888;
|
||||
margin-bottom: 0.7em;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.4em;
|
||||
}
|
||||
.partie p { font-size: 9.5pt; line-height: 1.7; }
|
||||
.partie .nom { font-weight: bold; font-size: 11pt; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
thead tr {
|
||||
background-color: #2c3e50;
|
||||
color: white;
|
||||
}
|
||||
thead th {
|
||||
padding: 0.6em 0.8em;
|
||||
text-align: left;
|
||||
font-size: 9pt;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
thead th:last-child { text-align: right; }
|
||||
tbody tr:nth-child(even) { background: #f8f9fa; }
|
||||
tbody td {
|
||||
padding: 0.6em 0.8em;
|
||||
font-size: 9.5pt;
|
||||
border-bottom: 1px solid #eee;
|
||||
vertical-align: top;
|
||||
}
|
||||
tbody td:last-child { text-align: right; font-weight: 500; }
|
||||
td.qte { text-align: center; }
|
||||
td.pu { text-align: right; }
|
||||
|
||||
.totaux {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.totaux table {
|
||||
width: auto;
|
||||
min-width: 280px;
|
||||
}
|
||||
.totaux td {
|
||||
padding: 0.4em 0.8em;
|
||||
font-size: 10pt;
|
||||
border: none;
|
||||
}
|
||||
.totaux tr.total-ht td {
|
||||
font-size: 12pt;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
border-top: 2px solid #2c3e50;
|
||||
padding-top: 0.6em;
|
||||
}
|
||||
.totaux td:last-child { text-align: right; }
|
||||
|
||||
.tva-mention {
|
||||
font-size: 8.5pt;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 1em;
|
||||
font-size: 8.5pt;
|
||||
color: #555;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.footer-info .conditions {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.footer-info .penalites {
|
||||
font-size: 8pt;
|
||||
color: #888;
|
||||
}
|
||||
.footer-info .iban {
|
||||
margin-top: 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.devis-ref {
|
||||
font-size: 8.5pt;
|
||||
color: #888;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.notes {
|
||||
background: #f8f9fa;
|
||||
border-left: 3px solid #2c3e50;
|
||||
padding: 0.7em 1em;
|
||||
font-size: 9pt;
|
||||
margin-bottom: 1.5em;
|
||||
color: #444;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- EN-TÊTE -->
|
||||
<div class="header">
|
||||
<div class="emetteur">
|
||||
<h1>{{ settings.asso_nom }}</h1>
|
||||
<p>
|
||||
{{ settings.asso_adresse }}<br>
|
||||
{{ settings.asso_code_postal }} {{ settings.asso_ville }}<br>
|
||||
{% if settings.asso_email %}{{ settings.asso_email }}<br>{% endif %}
|
||||
{% if settings.asso_telephone %}{{ settings.asso_telephone }}<br>{% endif %}
|
||||
{% if settings.asso_rna %}RNA : {{ settings.asso_rna }}<br>{% endif %}
|
||||
{% if settings.asso_siret %}SIRET : {{ settings.asso_siret }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="facture-titre">
|
||||
<h2>Facture</h2>
|
||||
<div class="numero">N° {{ facture.numero }}</div>
|
||||
<div class="dates">
|
||||
Date d'émission : {{ facture.date_emission.strftime('%d/%m/%Y') }}<br>
|
||||
Date d'échéance : {{ facture.date_echeance.strftime('%d/%m/%Y') }}
|
||||
{% if facture.statut.value == 'payee' and facture.date_paiement %}
|
||||
<br>Date de paiement : {{ facture.date_paiement.strftime('%d/%m/%Y') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PARTIES -->
|
||||
<div class="parties">
|
||||
<div class="partie">
|
||||
<h3>Émetteur</h3>
|
||||
<p>
|
||||
<span class="nom">{{ settings.asso_nom }}</span><br>
|
||||
{{ settings.asso_adresse }}<br>
|
||||
{{ settings.asso_code_postal }} {{ settings.asso_ville }}<br>
|
||||
{% if settings.asso_rna %}RNA : {{ settings.asso_rna }}<br>{% endif %}
|
||||
{% if settings.asso_siret %}SIRET : {{ settings.asso_siret }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="partie">
|
||||
<h3>Destinataire</h3>
|
||||
<p>
|
||||
<span class="nom">{{ facture.client.nom }}</span><br>
|
||||
{{ facture.client.adresse }}<br>
|
||||
{{ facture.client.code_postal }} {{ facture.client.ville }}
|
||||
{% if facture.client.siret %}<br>SIRET : {{ facture.client.siret }}{% endif %}
|
||||
{% if facture.client.email %}<br>{{ facture.client.email }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if facture.devis_origine %}
|
||||
<p class="devis-ref">Facture établie suite au devis {{ facture.devis_origine.numero }}</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- LIGNES -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:55%">Désignation</th>
|
||||
<th style="width:10%" class="qte">Qté</th>
|
||||
<th style="width:17%">Prix unitaire HT</th>
|
||||
<th style="width:18%">Total HT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in facture.lignes %}
|
||||
<tr>
|
||||
<td>{{ l.description }}</td>
|
||||
<td class="qte">
|
||||
{% if l.quantite == l.quantite | int %}{{ l.quantite | int }}{% else %}{{ l.quantite }}{% endif %}
|
||||
</td>
|
||||
<td class="pu">
|
||||
{{ "%.2f"|format(l.prix_unitaire_ht) | replace('.', ',') }} €
|
||||
</td>
|
||||
<td>{{ "%.2f"|format(l.total_ht) | replace('.', ',') }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- TOTAUX -->
|
||||
<div class="totaux">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Montant HT</td>
|
||||
<td>{{ "%.2f"|format(facture.total_ht) | replace('.', ',') }} €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TVA</td>
|
||||
<td>0,00 €</td>
|
||||
</tr>
|
||||
<tr class="total-ht">
|
||||
<td>Total TTC</td>
|
||||
<td>{{ "%.2f"|format(facture.total_ht) | replace('.', ',') }} €</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<p class="tva-mention">TVA non applicable — art. 293B du CGI</p>
|
||||
|
||||
{% if facture.notes %}
|
||||
<div class="notes">{{ facture.notes }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- PIED DE PAGE LÉGAL -->
|
||||
<div class="footer-info">
|
||||
<div class="conditions">
|
||||
<strong>Conditions de règlement :</strong>
|
||||
{{ facture.conditions_reglement or "Paiement à réception de facture." }}
|
||||
</div>
|
||||
<div class="penalites">
|
||||
En cas de retard de paiement, des pénalités de retard au taux de 3 fois le taux d'intérêt légal
|
||||
seront exigibles (art. L.441-10 du Code de commerce), ainsi qu'une indemnité forfaitaire de
|
||||
recouvrement de 40 € (décret n° 2012-1115).
|
||||
</div>
|
||||
{% if settings.asso_iban %}
|
||||
<div class="iban">
|
||||
Coordonnées bancaires — IBAN : {{ settings.asso_iban }}
|
||||
{% if settings.asso_bic %} — BIC : {{ settings.asso_bic }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user