Init project

This commit is contained in:
seb
2026-02-21 23:26:50 +01:00
parent df61e93871
commit b7046b125c
29 changed files with 2553 additions and 0 deletions

View 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
View 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 %}

View 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 %}