Init project
This commit is contained in:
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 %}
|
||||
Reference in New Issue
Block a user