Initial commit — 3D Pricing app
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}3D Pricing{% endblock %} — 3D Pricing</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
:root { --sidebar-w: 220px; }
|
||||
body { background: #f4f6f9; }
|
||||
|
||||
#sidebar {
|
||||
width: var(--sidebar-w); min-height: 100vh;
|
||||
background: #1a1d23; position: fixed; top: 0; left: 0;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
#sidebar .brand {
|
||||
padding: 1.25rem 1.25rem 1rem; color: #fff;
|
||||
font-weight: 700; font-size: 1.1rem;
|
||||
border-bottom: 1px solid #2e323d;
|
||||
}
|
||||
#sidebar .brand span { color: #ff6b35; }
|
||||
#sidebar nav { flex: 1; padding: .5rem 0; }
|
||||
#sidebar nav a {
|
||||
display: flex; align-items: center; gap: .65rem;
|
||||
padding: .6rem 1.25rem; color: #9ba3af;
|
||||
text-decoration: none; font-size: .875rem;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
#sidebar nav a:hover, #sidebar nav a.active { background: #2e323d; color: #fff; }
|
||||
#sidebar nav a.active { border-left: 3px solid #ff6b35; }
|
||||
#sidebar nav .nav-section {
|
||||
padding: .75rem 1.25rem .25rem; font-size: .7rem;
|
||||
text-transform: uppercase; letter-spacing: .08em; color: #4b5563;
|
||||
}
|
||||
|
||||
#main { margin-left: var(--sidebar-w); padding: 1.75rem 2rem; }
|
||||
.page-header { margin-bottom: 1.5rem; }
|
||||
.page-header h1 { font-size: 1.4rem; font-weight: 700; margin: 0; }
|
||||
|
||||
.stat-card {
|
||||
background: #fff; border-radius: 10px; padding: 1.25rem 1.5rem;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.06);
|
||||
}
|
||||
.stat-card .label { font-size: .75rem; color: #6b7280; text-transform: uppercase; letter-spacing: .05em; }
|
||||
.stat-card .value { font-size: 1.75rem; font-weight: 700; margin-top: .1rem; }
|
||||
|
||||
.card { border: none; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.06); }
|
||||
.card-header { background: #fff; border-bottom: 1px solid #f0f0f0; font-weight: 600; }
|
||||
|
||||
.breakdown-row { display: flex; justify-content: space-between; padding: .2rem 0; font-size: .88rem; }
|
||||
.breakdown-row.total { font-weight: 700; font-size: 1rem; border-top: 2px solid #dee2e6; padding-top: .5rem; margin-top: .3rem; }
|
||||
.breakdown-row.subtotal { border-top: 1px solid #dee2e6; padding-top: .4rem; margin-top: .2rem; }
|
||||
.breakdown-muted { color: #6b7280; }
|
||||
|
||||
.final-price { font-size: 2rem; font-weight: 700; color: #ff6b35; }
|
||||
|
||||
.stock-ok { color: #22c55e; }
|
||||
.stock-low { color: #f59e0b; }
|
||||
.stock-empty { color: #ef4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="sidebar">
|
||||
<div class="brand"><i class="bi bi-layers-half"></i> 3D<span>Pricing</span></div>
|
||||
<nav>
|
||||
<div class="nav-section">Navigation</div>
|
||||
<a href="{{ url_for('dashboard') }}" class="{% if request.endpoint=='dashboard' %}active{% endif %}">
|
||||
<i class="bi bi-speedometer2"></i> Dashboard
|
||||
</a>
|
||||
<a href="{{ url_for('jobs') }}" class="{% if request.endpoint in ['jobs','job_detail'] %}active{% endif %}">
|
||||
<i class="bi bi-printer"></i> Commandes
|
||||
</a>
|
||||
<a href="{{ url_for('new_job') }}" class="{% if request.endpoint=='new_job' %}active{% endif %}">
|
||||
<i class="bi bi-plus-circle"></i> Nouveau calcul
|
||||
</a>
|
||||
<a href="{{ url_for('clients') }}" class="{% if 'client' in request.endpoint %}active{% endif %}">
|
||||
<i class="bi bi-building"></i> Clients
|
||||
</a>
|
||||
<a href="{{ url_for('materials') }}" class="{% if 'material' in request.endpoint %}active{% endif %}">
|
||||
<i class="bi bi-boxes"></i> Matières
|
||||
</a>
|
||||
<div class="nav-section">Analyse</div>
|
||||
<a href="{{ url_for('stats') }}" class="{% if request.endpoint=='stats' %}active{% endif %}">
|
||||
<i class="bi bi-bar-chart-line"></i> Statistiques
|
||||
</a>
|
||||
<a href="{{ url_for('export_csv') }}">
|
||||
<i class="bi bi-download"></i> Export CSV
|
||||
</a>
|
||||
<div class="nav-section">Config</div>
|
||||
<a href="{{ url_for('settings') }}" class="{% if request.endpoint=='settings' %}active{% endif %}">
|
||||
<i class="bi bi-sliders"></i> Paramètres
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, msg in messages %}
|
||||
<div class="alert alert-{{ cat }} alert-dismissible fade show">
|
||||
{{ msg }} <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{% if client %}Modifier{% else %}Nouveau{% endif %} client{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<a href="{{ url_for('clients') }}" class="text-muted text-decoration-none small">
|
||||
<i class="bi bi-arrow-left"></i> Clients
|
||||
</a>
|
||||
<h1 class="mt-1">{% if client %}Modifier {{ client.name }}{% else %}Nouveau client{% endif %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Nom *</label>
|
||||
<input type="text" name="name" class="form-control" required
|
||||
value="{{ client.name if client else '' }}" placeholder="ex: Blockcorp">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Email</label>
|
||||
<input type="email" name="email" class="form-control"
|
||||
value="{{ client.email if client else '' }}">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Coefficient design par défaut</label>
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-success" onclick="setDM(0.80)">
|
||||
×0.80 — STL fourni
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="setDM(1.80)">
|
||||
×1.80 — Création complète
|
||||
</button>
|
||||
</div>
|
||||
<input type="number" name="design_multiplier" id="dm" class="form-control"
|
||||
step="0.05" min="0" value="{{ client.design_multiplier if client else '1.80' }}">
|
||||
<div class="form-text">0.80 = client qui fournit ses STL. 1.80 = vous faites tout le design.</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Notes</label>
|
||||
<textarea name="notes" class="form-control" rows="3">{{ client.notes if client else '' }}</textarea>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn" style="background:#ff6b35;color:#fff">
|
||||
<i class="bi bi-save me-1"></i>Enregistrer
|
||||
</button>
|
||||
<a href="{{ url_for('clients') }}" class="btn btn-outline-secondary">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function setDM(val) {
|
||||
document.getElementById('dm').value = val;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Clients{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header d-flex justify-content-between align-items-center">
|
||||
<h1><i class="bi bi-building me-2 text-info"></i>Clients</h1>
|
||||
<a href="{{ url_for('new_client') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
||||
<i class="bi bi-plus-lg me-1"></i>Nouveau client
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
{% if clients %}
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nom</th><th>Email</th><th>Coeff design par défaut</th>
|
||||
<th>Commandes</th><th>CA total</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in clients %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ c.name }}</td>
|
||||
<td class="text-muted">{{ c.email or '—' }}</td>
|
||||
<td>
|
||||
{% if c.design_multiplier <= 0.80 %}
|
||||
<span class="badge bg-success">×{{ c.design_multiplier }} (STL fourni)</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">×{{ c.design_multiplier }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ c.job_count }}</td>
|
||||
<td class="fw-semibold">{{ c.total_revenue or 0 }} €</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ url_for('edit_client', id=c.id) }}" class="btn btn-sm btn-outline-secondary py-0">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('delete_client', id=c.id) }}"
|
||||
onsubmit="return confirm('Supprimer ce client ? Ses commandes seront conservées.')">
|
||||
<button class="btn btn-sm btn-outline-danger py-0"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-building fs-1"></i>
|
||||
<p class="mt-2">Aucun client enregistré.</p>
|
||||
<a href="{{ url_for('new_client') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">Ajouter le premier client</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Binary file not shown.
@@ -0,0 +1,102 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ job.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<a href="{{ url_for('jobs') }}" class="text-muted text-decoration-none small">
|
||||
<i class="bi bi-arrow-left"></i> Commandes
|
||||
</a>
|
||||
<h1 class="mt-1">{{ job.name }}</h1>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('delete_job', id=job.id) }}"
|
||||
onsubmit="return confirm('Supprimer cette commande ?')">
|
||||
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i> Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-5">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-3">Informations</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
<tr><th class="text-muted fw-normal" style="width:45%">Client</th><td>{{ job.client_name or '—' }}</td></tr>
|
||||
<tr><th class="text-muted fw-normal">Matière</th>
|
||||
<td>
|
||||
{% if job.material_name %}
|
||||
<span class="badge bg-secondary">{{ job.material_type }}</span> {{ job.material_name }}
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr><th class="text-muted fw-normal">Fichier source</th>
|
||||
<td class="text-monospace small">{{ job.source_file or '—' }}</td></tr>
|
||||
<tr><th class="text-muted fw-normal">Date</th><td>{{ job.created_at[:10] }}</td></tr>
|
||||
<tr><th class="text-muted fw-normal">Poids filament</th><td>{{ job.weight_g }} g</td></tr>
|
||||
<tr><th class="text-muted fw-normal">Durée impression</th><td>{{ job.print_time_s | fmt_time }}</td></tr>
|
||||
<tr><th class="text-muted fw-normal">Coeff design</th><td>×{{ job.design_multiplier }}</td></tr>
|
||||
<tr><th class="text-muted fw-normal">Marge brute</th><td>{{ job.gross_margin_pct }} %</td></tr>
|
||||
<tr><th class="text-muted fw-normal">Remise</th><td>{{ job.discount_pct }} %</td></tr>
|
||||
</table>
|
||||
{% if job.notes %}
|
||||
<hr>
|
||||
<small class="text-muted">{{ job.notes }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-7">
|
||||
<div class="card">
|
||||
<div class="card-header py-3"><i class="bi bi-receipt me-2"></i>Décomposition du prix</div>
|
||||
<div class="card-body">
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Matière</span><span>{{ "%.4f"|format(job.material_cost) }} €</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Marge matière</span><span>{{ "%.4f"|format(job.material_margin) }} €</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Design (×{{ job.design_multiplier }})</span><span>{{ "%.4f"|format(job.design_cost) }} €</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Manutention</span><span>{{ "%.4f"|format(job.handling_cost) }} €</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Usure machine</span><span>{{ "%.4f"|format(job.wear_cost) }} €</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Électricité</span><span>{{ "%.4f"|format(job.electricity_cost) }} €</span></div>
|
||||
<div class="breakdown-row subtotal">
|
||||
<span class="fw-semibold">Sous-total coûts</span>
|
||||
<span class="fw-semibold">{{ "%.2f"|format(job.subtotal) }} €</span>
|
||||
</div>
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-muted">Marge brute ({{ job.gross_margin_pct }}%)</span>
|
||||
<span>{{ "%.2f"|format(job.margin_amount) }} €</span>
|
||||
</div>
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-muted">Cotisations micro-ent.</span>
|
||||
<span>{{ "%.2f"|format(job.tax_amount) }} €</span>
|
||||
</div>
|
||||
{% if job.discount_pct > 0 %}
|
||||
<div class="breakdown-row text-danger">
|
||||
<span>Remise ({{ job.discount_pct }}%)</span>
|
||||
<span>−{{ "%.2f"|format((job.subtotal + job.margin_amount + job.tax_amount) * job.discount_pct / 100) }} €</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="breakdown-row total">
|
||||
<span>PRIX FINAL</span>
|
||||
<span class="final-price">{{ "%.2f"|format(job.final_price) }} €</span>
|
||||
</div>
|
||||
|
||||
<!-- Barre de composition -->
|
||||
{% set total = job.final_price %}
|
||||
{% if total > 0 %}
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between small text-muted mb-1">
|
||||
<span>Coûts ({{ (job.subtotal/total*100)|round(1) }}%)</span>
|
||||
<span>Marge ({{ (job.margin_amount/total*100)|round(1) }}%)</span>
|
||||
<span>Taxes ({{ (job.tax_amount/total*100)|round(1) }}%)</span>
|
||||
</div>
|
||||
<div class="progress" style="height:18px;border-radius:6px">
|
||||
<div class="progress-bar bg-secondary" style="width:{{ (job.subtotal/total*100)|round(1) }}%"></div>
|
||||
<div class="progress-bar" style="width:{{ (job.margin_amount/total*100)|round(1) }}%;background:#ff6b35"></div>
|
||||
<div class="progress-bar bg-warning text-dark" style="width:{{ (job.tax_amount/total*100)|round(1) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Commandes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header d-flex justify-content-between align-items-center">
|
||||
<h1><i class="bi bi-printer me-2 text-primary"></i>Commandes</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('export_csv') }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-download me-1"></i>Export CSV
|
||||
</a>
|
||||
<a href="{{ url_for('new_job') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
||||
<i class="bi bi-plus-lg me-1"></i>Nouveau
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
{% if jobs %}
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th><th>Nom</th><th>Client</th><th>Poids</th>
|
||||
<th>Durée</th><th>Design</th><th>Marge</th><th>Remise</th>
|
||||
<th class="text-end">Prix final</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for j in jobs %}
|
||||
<tr>
|
||||
<td class="text-muted small">{{ j.created_at[:10] }}</td>
|
||||
<td><a href="{{ url_for('job_detail', id=j.id) }}" class="text-decoration-none fw-semibold">{{ j.name }}</a></td>
|
||||
<td>{{ j.client_name or '—' }}</td>
|
||||
<td>{{ j.weight_g }} g</td>
|
||||
<td>{{ j.print_time_s | fmt_time }}</td>
|
||||
<td><span class="badge bg-light text-dark">×{{ j.design_multiplier }}</span></td>
|
||||
<td><span class="badge bg-light text-dark">{{ j.gross_margin_pct }}%</span></td>
|
||||
<td>
|
||||
{% if j.discount_pct > 0 %}
|
||||
<span class="badge bg-danger">-{{ j.discount_pct }}%</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="text-end fw-bold" style="color:#ff6b35">{{ j.final_price }} €</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ url_for('job_detail', id=j.id) }}" class="btn btn-sm btn-outline-secondary py-0">→</a>
|
||||
<form method="POST" action="{{ url_for('delete_job', id=j.id) }}"
|
||||
onsubmit="return confirm('Supprimer ?')">
|
||||
<button class="btn btn-sm btn-outline-danger py-0"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-inbox fs-1"></i>
|
||||
<p class="mt-2">Aucune commande enregistrée.</p>
|
||||
<a href="{{ url_for('new_job') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">Créer le premier calcul</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,100 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{% if material %}Modifier{% else %}Nouvelle{% endif %} matière{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<a href="{{ url_for('materials') }}" class="text-muted text-decoration-none small">
|
||||
<i class="bi bi-arrow-left"></i> Matières
|
||||
</a>
|
||||
<h1 class="mt-1">{% if material %}Modifier — {{ material.name }}{% else %}Nouvelle matière{% endif %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="row g-3">
|
||||
<div class="col-8">
|
||||
<label class="form-label fw-semibold">Nom *</label>
|
||||
<input type="text" name="name" class="form-control" required
|
||||
value="{{ material.name if material else '' }}"
|
||||
placeholder="ex: PLA+ Rouge Bambu">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label fw-semibold">Type</label>
|
||||
<select name="type" class="form-select">
|
||||
{% for t in ['PLA','PLA+','PETG','ASA','ABS','TPU','Nylon','PC','Résine','Autre'] %}
|
||||
<option value="{{ t }}" {% if material and material.type==t %}selected{% endif %}>{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label fw-semibold">Marque</label>
|
||||
<input type="text" name="brand" class="form-control"
|
||||
value="{{ material.brand if material else '' }}"
|
||||
placeholder="Bambu, eSUN, Polymaker…">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label fw-semibold">Couleur</label>
|
||||
<input type="text" name="color" class="form-control"
|
||||
value="{{ material.color if material else '' }}"
|
||||
placeholder="Rouge, #FF0000…">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label fw-semibold">Prix (€/kg) *</label>
|
||||
<input type="number" name="price_per_kg" class="form-control"
|
||||
step="0.01" min="0" required
|
||||
value="{{ material.price_per_kg if material else '25.00' }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label fw-semibold">Stock actuel (g)</label>
|
||||
<input type="number" name="stock_g" class="form-control"
|
||||
step="1" min="0"
|
||||
value="{{ material.stock_g|round(0)|int if material else 0 }}">
|
||||
<div class="form-text">1 rouleau ≈ 1000 g</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label fw-semibold">Seuil alerte stock bas (g)</label>
|
||||
<input type="number" name="low_stock_threshold_g" class="form-control"
|
||||
step="50" min="0"
|
||||
value="{{ material.low_stock_threshold_g|round(0)|int if material else 200 }}">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Notes</label>
|
||||
<textarea name="notes" class="form-control" rows="2">{{ material.notes if material else '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button type="submit" class="btn" style="background:#ff6b35;color:#fff">
|
||||
<i class="bi bi-save me-1"></i>Enregistrer
|
||||
</button>
|
||||
<a href="{{ url_for('materials') }}" class="btn btn-outline-secondary">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if history %}
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header py-3"><i class="bi bi-clock-history me-2"></i>Historique des prix</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light"><tr><th>Date</th><th class="text-end">€/kg</th></tr></thead>
|
||||
<tbody>
|
||||
{% for h in history %}
|
||||
<tr>
|
||||
<td class="text-muted small">{{ h.recorded_at[:10] }}</td>
|
||||
<td class="text-end fw-semibold">{{ h.price_per_kg }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,120 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Matières{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header d-flex justify-content-between align-items-center">
|
||||
<h1><i class="bi bi-boxes me-2" style="color:#8b5cf6"></i>Matières & Stock</h1>
|
||||
<a href="{{ url_for('new_material') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
||||
<i class="bi bi-plus-lg me-1"></i>Ajouter une matière
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
{% if materials %}
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nom</th><th>Type</th><th>Couleur</th>
|
||||
<th>Prix/kg</th><th>Stock</th><th>Commandes</th><th>Utilisé total</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in materials %}
|
||||
{% set stock_pct = [[(m.stock_g / 1000 * 100)|round(0)|int, 0]|max, 100]|min %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ m.name }}{% if m.brand %} <small class="text-muted">/ {{ m.brand }}</small>{% endif %}</td>
|
||||
<td><span class="badge bg-secondary">{{ m.type }}</span></td>
|
||||
<td>
|
||||
{% if m.color %}
|
||||
<span class="d-inline-flex align-items-center gap-1">
|
||||
<span style="width:14px;height:14px;border-radius:50%;background:{{ m.color }};border:1px solid #ccc;display:inline-block"></span>
|
||||
{{ m.color }}
|
||||
</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="fw-semibold">{{ m.price_per_kg }} €</td>
|
||||
<td style="min-width:140px">
|
||||
{% if m.stock_g <= 0 %}
|
||||
<span class="stock-empty fw-bold"><i class="bi bi-x-circle"></i> Rupture</span>
|
||||
{% elif m.stock_g <= m.low_stock_threshold_g %}
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="stock-low"><i class="bi bi-exclamation-triangle"></i> {{ m.stock_g|round(0)|int }} g</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="stock-ok"><i class="bi bi-check-circle"></i> {{ m.stock_g|round(0)|int }} g</span>
|
||||
{% endif %}
|
||||
<div class="progress mt-1" style="height:4px">
|
||||
<div class="progress-bar
|
||||
{% if m.stock_g <= 0 %}bg-danger
|
||||
{% elif m.stock_g <= m.low_stock_threshold_g %}bg-warning
|
||||
{% else %}bg-success{% endif %}"
|
||||
style="width:{{ [[(m.stock_g / 1000 * 100)|round(0)|int, 0]|max, 100]|min }}%">
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ m.job_count }}</td>
|
||||
<td>{{ m.total_used_g }} g</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-outline-success py-0" data-bs-toggle="modal"
|
||||
data-bs-target="#restockModal{{ m.id }}">
|
||||
<i class="bi bi-plus-lg"></i> Stock
|
||||
</button>
|
||||
<a href="{{ url_for('edit_material', id=m.id) }}" class="btn btn-sm btn-outline-secondary py-0">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('delete_material', id=m.id) }}"
|
||||
onsubmit="return confirm('Supprimer cette matière ?')">
|
||||
<button class="btn btn-sm btn-outline-danger py-0"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Modal Restock -->
|
||||
<div class="modal fade" id="restockModal{{ m.id }}" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="{{ url_for('restock_material', id=m.id) }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Réapprovisionner — {{ m.name }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Quantité à ajouter (g)</label>
|
||||
<input type="number" name="add_g" class="form-control" step="1" min="1"
|
||||
placeholder="ex: 1000 pour 1 kg" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nouveau prix/kg (optionnel)</label>
|
||||
<input type="number" name="new_price_per_kg" class="form-control"
|
||||
step="0.01" placeholder="{{ m.price_per_kg }} (actuel)">
|
||||
<div class="form-text">Laissez vide si le prix n'a pas changé.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-success btn-sm">Ajouter au stock</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-boxes fs-1"></i>
|
||||
<p class="mt-2">Aucune matière enregistrée.</p>
|
||||
<a href="{{ url_for('new_material') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
||||
Ajouter votre premier filament
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,233 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Nouveau calcul{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1><i class="bi bi-plus-circle me-2" style="color:#ff6b35"></i>Nouveau calcul de prix</h1>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-7">
|
||||
<!-- Import 3mf -->
|
||||
<div class="card mb-3 border border-warning border-opacity-50">
|
||||
<div class="card-header py-3">
|
||||
<i class="bi bi-file-earmark-zip text-warning me-2"></i>Import Bambu Studio (.3mf)
|
||||
<span class="badge bg-warning text-dark ms-1">Optionnel</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<input type="file" id="file3mf" accept=".3mf" class="form-control" style="max-width:320px">
|
||||
<button type="button" class="btn btn-outline-warning" onclick="parse3mf()">
|
||||
<i class="bi bi-magic me-1"></i>Extraire
|
||||
</button>
|
||||
<span id="parseStatus" class="text-muted small"></span>
|
||||
</div>
|
||||
<div class="form-text mt-1">Glissez votre fichier .3mf depuis Bambu Studio pour remplir poids et duree automatiquement.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" id="jobForm">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-3">Donnees impression</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label fw-semibold">Nom de la commande</label>
|
||||
<input type="text" name="name" id="jobName" class="form-control" required placeholder="ex: Marqueurs competition salle X">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Fichier source</label>
|
||||
<input type="text" name="source_file" id="sourceFile" class="form-control" placeholder="nom_fichier.3mf">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Client</label>
|
||||
<select name="client_id" id="clientSelect" class="form-select">
|
||||
<option value="">Sans client</option>
|
||||
{% for c in clients %}
|
||||
<option value="{{ c.id }}" data-dm="{{ c.design_multiplier }}">{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Matiere</label>
|
||||
<select name="material_id" id="materialSelect" class="form-select">
|
||||
<option value="">Prix global (parametres)</option>
|
||||
{% for m in materials %}
|
||||
<option value="{{ m.id }}" data-price="{{ m.price_per_kg }}" data-stock="{{ m.stock_g }}" data-threshold="{{ m.low_stock_threshold_g }}">
|
||||
{{ m.name }} ({{ m.type }}) - {{ m.price_per_kg }}EUR/kg
|
||||
{% if m.stock_g <= 0 %}[RUPTURE]{% elif m.stock_g <= m.low_stock_threshold_g %}[stock bas]{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div id="stockInfo" class="form-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Poids filament (g)</label>
|
||||
<input type="number" name="weight_g" id="weight_g" class="form-control" step="0.01" min="0.1" required placeholder="423.97">
|
||||
<div class="form-text">Champ Modele dans Bambu</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Heures</label>
|
||||
<input type="number" name="hours" id="hours" class="form-control" min="0" value="0">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Minutes</label>
|
||||
<input type="number" name="minutes" id="minutes" class="form-control" min="0" max="59" value="0">
|
||||
<div class="form-text">Temps total Bambu</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header py-3">Parametres tarifaires</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">
|
||||
Coefficient design <span class="badge bg-secondary ms-1" id="dmLabel">x1.80</span>
|
||||
</label>
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-success" onclick="setDM(0.80)">STL fourni (x0.80)</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="setDM(1.80)">Creation complete (x1.80)</button>
|
||||
</div>
|
||||
<input type="number" name="design_multiplier" id="design_multiplier" class="form-control form-control-sm" step="0.05" min="0" value="1.80">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold d-flex justify-content-between">
|
||||
Marge brute <span id="marginDisplay" class="text-muted">27.5 %</span>
|
||||
</label>
|
||||
<input type="range" name="gross_margin_pct" id="gross_margin_pct" class="form-range" min="20" max="35" step="0.5" value="27.5">
|
||||
<div class="d-flex justify-content-between" style="font-size:.75rem;color:#6b7280"><span>20%</span><span>35%</span></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Remise client (%)</label>
|
||||
<input type="number" name="discount_pct" id="discount_pct" class="form-control" min="0" max="100" step="0.5" value="0">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Notes internes</label>
|
||||
<textarea name="notes" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn w-100 py-2 fw-bold" style="background:#ff6b35;color:#fff">
|
||||
<i class="bi bi-save me-2"></i>Enregistrer la commande
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<div class="card sticky-top" style="top:1.5rem">
|
||||
<div class="card-header py-3"><i class="bi bi-receipt me-2"></i>Recapitulatif temps reel</div>
|
||||
<div class="card-body" id="breakdown">
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="bi bi-calculator fs-2"></i>
|
||||
<p class="mt-2 small">Remplissez le poids et la duree pour voir le calcul.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function parse3mf() {
|
||||
const file = document.getElementById('file3mf').files[0];
|
||||
if (!file) { alert('Selectionnez un fichier .3mf'); return; }
|
||||
const status = document.getElementById('parseStatus');
|
||||
status.textContent = 'Analyse...';
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
fetch('/api/parse-3mf', {method:'POST', body:fd})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
let filled = [];
|
||||
if (d.weight_g) { document.getElementById('weight_g').value = d.weight_g; filled.push('poids'); }
|
||||
if (d.hours != null){ document.getElementById('hours').value = d.hours; filled.push('heures'); }
|
||||
if (d.minutes != null){ document.getElementById('minutes').value = d.minutes; }
|
||||
if (d.filename) {
|
||||
document.getElementById('sourceFile').value = d.filename;
|
||||
if (!document.querySelector('[name=name]').value)
|
||||
document.querySelector('[name=name]').value = d.filename;
|
||||
}
|
||||
status.textContent = filled.length ? 'Extrait: '+filled.join(', ') : 'Donnees non trouvees';
|
||||
status.style.color = filled.length ? '#22c55e' : '#f59e0b';
|
||||
recalc();
|
||||
})
|
||||
.catch(() => { status.textContent = 'Erreur de lecture'; status.style.color='#ef4444'; });
|
||||
}
|
||||
|
||||
document.getElementById('materialSelect').addEventListener('change', function() {
|
||||
const opt = this.options[this.selectedIndex];
|
||||
const stock = parseFloat(opt.dataset.stock)||0;
|
||||
const threshold = parseFloat(opt.dataset.threshold)||200;
|
||||
const info = document.getElementById('stockInfo');
|
||||
if (!opt.value) { info.textContent=''; return; }
|
||||
if (stock<=0) info.innerHTML='<span class="text-danger">RUPTURE DE STOCK</span>';
|
||||
else if (stock<=threshold) info.innerHTML='<span class="text-warning">Stock bas: '+stock.toFixed(0)+' g</span>';
|
||||
else info.innerHTML='<span class="text-success">Stock: '+stock.toFixed(0)+' g</span>';
|
||||
recalc();
|
||||
});
|
||||
|
||||
function setDM(val) {
|
||||
document.getElementById('design_multiplier').value = val;
|
||||
document.getElementById('dmLabel').textContent = 'x'+val.toFixed(2);
|
||||
recalc();
|
||||
}
|
||||
document.getElementById('gross_margin_pct').addEventListener('input', function(){
|
||||
document.getElementById('marginDisplay').textContent = this.value+' %'; recalc();
|
||||
});
|
||||
document.getElementById('design_multiplier').addEventListener('input', function(){
|
||||
document.getElementById('dmLabel').textContent = 'x'+parseFloat(this.value).toFixed(2); recalc();
|
||||
});
|
||||
document.getElementById('clientSelect').addEventListener('change', function(){
|
||||
const dm = this.options[this.selectedIndex].dataset.dm;
|
||||
if (dm) setDM(parseFloat(dm));
|
||||
});
|
||||
['weight_g','hours','minutes','discount_pct'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener('input', recalc);
|
||||
});
|
||||
|
||||
function recalc() {
|
||||
const w = parseFloat(document.getElementById('weight_g').value)||0;
|
||||
const h = parseInt(document.getElementById('hours').value)||0;
|
||||
const m = parseInt(document.getElementById('minutes').value)||0;
|
||||
if (w<=0 || (h===0 && m===0)) return;
|
||||
const body = {
|
||||
weight_g: w, hours: h, minutes: m,
|
||||
design_multiplier: parseFloat(document.getElementById('design_multiplier').value)||1.80,
|
||||
gross_margin_pct: parseFloat(document.getElementById('gross_margin_pct').value)||27.5,
|
||||
discount_pct: parseFloat(document.getElementById('discount_pct').value)||0,
|
||||
material_id: document.getElementById('materialSelect').value||null,
|
||||
};
|
||||
fetch('/api/calculate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
|
||||
.then(r=>r.json()).then(d=>renderBreakdown(d,body));
|
||||
}
|
||||
|
||||
function fmt(n) { return parseFloat(n).toFixed(4)+' EUR'; }
|
||||
function fmt2(n) { return parseFloat(n).toFixed(2)+' EUR'; }
|
||||
|
||||
function renderBreakdown(d, body) {
|
||||
const dis = body.discount_pct>0
|
||||
? '<div class="breakdown-row text-danger"><span>Remise ('+body.discount_pct+'%)</span></div>' : '';
|
||||
document.getElementById('breakdown').innerHTML = `
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Matiere</span><span>${fmt(d.material_cost)}</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Marge matiere (20%)</span><span>${fmt(d.material_margin)}</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Design (x${body.design_multiplier.toFixed(2)})</span><span>${fmt(d.design_cost)}</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Manutention</span><span>${fmt(d.handling_cost)}</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Usure machine</span><span>${fmt(d.wear_cost)}</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Electricite</span><span>${fmt(d.electricity_cost)}</span></div>
|
||||
<div class="breakdown-row subtotal"><span class="fw-semibold">Sous-total couts</span><span class="fw-semibold">${fmt2(d.subtotal)}</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Marge brute (${body.gross_margin_pct}%)</span><span>${fmt2(d.margin_amount)}</span></div>
|
||||
<div class="breakdown-row"><span class="breakdown-muted">Cotisations (12.3%)</span><span>${fmt2(d.tax_amount)}</span></div>
|
||||
${dis}
|
||||
<div class="breakdown-row total"><span>PRIX FINAL</span><span class="final-price">${fmt2(d.final_price)}</span></div>`;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,132 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Paramètres{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1><i class="bi bi-sliders me-2 text-secondary"></i>Paramètres</h1>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<div class="row g-4">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header py-3"><i class="bi bi-boxes me-2"></i>Filament</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Prix du filament (€/kg)</label>
|
||||
<input type="number" name="filament_price_per_kg" class="form-control"
|
||||
step="0.01" value="{{ settings.filament_price_per_kg }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Marge matière (%)</label>
|
||||
<input type="number" name="material_margin_pct" class="form-control"
|
||||
step="0.5" value="{{ settings.material_margin_pct }}">
|
||||
<div class="form-text">Appliquée sur le coût matière brut.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header py-3"><i class="bi bi-lightning-charge me-2"></i>Électricité</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Prix kWh (€)</label>
|
||||
<input type="number" name="electricity_price_kwh" class="form-control"
|
||||
step="0.001" value="{{ settings.electricity_price_kwh }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Puissance imprimante (kW)</label>
|
||||
<input type="number" name="printer_power_kw" class="form-control"
|
||||
step="0.01" value="{{ settings.printer_power_kw }}">
|
||||
<div class="form-text">Bambu X1C ≈ 0.35 kW en moyenne.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header py-3"><i class="bi bi-gear me-2"></i>Usure machine</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Prix imprimante (€)</label>
|
||||
<input type="number" name="printer_price" class="form-control"
|
||||
step="1" value="{{ settings.printer_price }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Durée de vie (h)</label>
|
||||
<input type="number" name="printer_lifespan_hours" class="form-control"
|
||||
step="100" value="{{ settings.printer_lifespan_hours }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Prix buse (€)</label>
|
||||
<input type="number" name="nozzle_price" class="form-control"
|
||||
step="0.5" value="{{ settings.nozzle_price }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Durée de vie buse (h)</label>
|
||||
<input type="number" name="nozzle_lifespan_hours" class="form-control"
|
||||
step="10" value="{{ settings.nozzle_lifespan_hours }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Prix plateau (€)</label>
|
||||
<input type="number" name="plate_price" class="form-control"
|
||||
step="0.5" value="{{ settings.plate_price }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Durée de vie plateau (h)</label>
|
||||
<input type="number" name="plate_lifespan_hours" class="form-control"
|
||||
step="50" value="{{ settings.plate_lifespan_hours }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header py-3"><i class="bi bi-hand-index me-2"></i>Manutention</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Taux horaire manutention (€/h)</label>
|
||||
<input type="number" name="handling_rate_per_hour" class="form-control"
|
||||
step="1" value="{{ settings.handling_rate_per_hour }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Temps par plateau (minutes)</label>
|
||||
<input type="number" name="handling_minutes_per_plate" class="form-control"
|
||||
step="1" value="{{ settings.handling_minutes_per_plate }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header py-3"><i class="bi bi-percent me-2"></i>Fiscal</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Taux cotisations micro-entrepreneur (%)</label>
|
||||
<input type="number" name="tax_rate_pct" class="form-control"
|
||||
step="0.1" value="{{ settings.tax_rate_pct }}">
|
||||
<div class="form-text">12.3% pour vente de marchandises (2024).</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn px-4 py-2 fw-bold" style="background:#ff6b35;color:#fff">
|
||||
<i class="bi bi-save me-2"></i>Enregistrer les paramètres
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,84 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Statistiques{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header d-flex justify-content-between align-items-center">
|
||||
<h1><i class="bi bi-bar-chart-line me-2 text-success"></i>Statistiques</h1>
|
||||
<a href="{{ url_for('export_csv') }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-download me-1"></i>Export CSV
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Par client -->
|
||||
<div class="col-md-7">
|
||||
<div class="card">
|
||||
<div class="card-header py-3">CA par client</div>
|
||||
<div class="card-body p-0">
|
||||
{% if by_client %}
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr><th>Client</th><th>Commandes</th><th>Marge moy.</th><th class="text-end">CA total</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set total_rev = by_client | sum(attribute='revenue') %}
|
||||
{% for c in by_client %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ c.name }}</td>
|
||||
<td>{{ c.jobs }}</td>
|
||||
<td><span class="badge bg-light text-dark">{{ c.avg_margin }}%</span></td>
|
||||
<td class="text-end">
|
||||
<span class="fw-bold" style="color:#ff6b35">{{ c.revenue }} €</span>
|
||||
{% if total_rev > 0 %}
|
||||
<div class="progress mt-1" style="height:4px">
|
||||
<div class="progress-bar" style="width:{{ (c.revenue / total_rev * 100)|round(0) }}%;background:#ff6b35"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">Aucune donnée disponible.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Par mois -->
|
||||
<div class="col-md-5">
|
||||
<div class="card">
|
||||
<div class="card-header py-3">CA par mois (12 derniers)</div>
|
||||
<div class="card-body p-0">
|
||||
{% if by_month %}
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr><th>Mois</th><th>Cmds</th><th class="text-end">CA</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set max_rev = by_month | map(attribute='revenue') | max %}
|
||||
{% for m in by_month %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ m.month }}</td>
|
||||
<td>{{ m.jobs }}</td>
|
||||
<td class="text-end">
|
||||
<span class="fw-bold">{{ m.revenue }} €</span>
|
||||
{% if max_rev > 0 %}
|
||||
<div class="progress mt-1" style="height:4px">
|
||||
<div class="progress-bar bg-success" style="width:{{ (m.revenue / max_rev * 100)|round(0) }}%"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">Aucune donnée disponible.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user