Files
3d-pricing/templates/new_job.html
T

420 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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">Votre fichier .3mf depuis Bambu Studio pour remplir poids et duree automatiquement.</div>
</div>
</div>
<form method="POST" id="jobForm">
<!-- Donnees impression -->
<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 (filament)</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 }}"
data-name="{{ m.name }} ({{ m.type }})">
{{ m.name }} ({{ m.type }}) — {{ m.price_per_kg }} €/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>
<!-- Profils -->
<div class="card mb-3">
<div class="card-header py-3">
<i class="bi bi-bookmark-star me-2"></i>Profils
<a href="{{ url_for('profiles') }}" class="btn btn-sm btn-outline-secondary ms-2 py-0" target="_blank">
<i class="bi bi-pencil-square"></i> Gerer
</a>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold small">
<i class="bi bi-cpu me-1 text-muted"></i>Profil machine
</label>
<select name="machine_profile_id" id="machineProfileSelect" class="form-select form-select-sm">
<option value="">— Parametres globaux —</option>
{% for p in machine_profiles %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold small">
<i class="bi bi-hand-index me-1 text-muted"></i>Profil manutention
</label>
<select name="handling_profile_id" id="handlingProfileSelect" class="form-select form-select-sm">
<option value="">— Parametres globaux —</option>
{% for p in handling_profiles %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold small">
<i class="bi bi-box me-1 text-muted"></i>Profil matiere
</label>
<select name="material_profile_id" id="materialProfileSelect" class="form-select form-select-sm">
<option value="">— Parametres globaux —</option>
{% for p in material_profiles %}
<option value="{{ p.id }}" data-margin="{{ p.material_margin_pct }}">
{{ p.name }} ({{ p.material_margin_pct }}%)
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold small">
<i class="bi bi-tags me-1 text-muted"></i>Profil tarif client
</label>
<select name="pricing_profile_id" id="pricingProfileSelect" class="form-select form-select-sm">
<option value="">— Manuel —</option>
{% for p in pricing_profiles %}
<option value="{{ p.id }}"
data-dm="{{ p.design_multiplier }}"
data-margin="{{ p.gross_margin_pct }}">
{{ p.name }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- Parametres tarifaires -->
<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" id="dmButtons">
<button type="button" id="btnDM080" class="btn btn-sm btn-outline-success" onclick="setDM(0.80)">
<i class="bi bi-check2 me-1 d-none" id="checkDM080"></i>STL fourni (x0.80)
</button>
<button type="button" id="btnDM180" class="btn btn-sm btn-outline-primary" onclick="setDM(1.80)">
<i class="bi bi-check2 me-1 d-none" id="checkDM180"></i>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="fw-bold" style="color:#ff6b35">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>
<!-- Recap temps reel -->
<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>
// ── 3MF parser ────────────────────────────────────────────────────────────────
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.getElementById('jobName').value)
document.getElementById('jobName').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'; });
}
// ── Coefficient design ────────────────────────────────────────────────────────
function setDM(val) {
document.getElementById('design_multiplier').value = val;
updateDMLabel(val);
recalc();
}
function updateDMLabel(val) {
val = parseFloat(val);
document.getElementById('dmLabel').textContent = 'x' + val.toFixed(2);
// Etat actif des boutons
const is080 = Math.abs(val - 0.80) < 0.001;
const is180 = Math.abs(val - 1.80) < 0.001;
document.getElementById('btnDM080').className = 'btn btn-sm ' + (is080 ? 'btn-success' : 'btn-outline-success');
document.getElementById('btnDM180').className = 'btn btn-sm ' + (is180 ? 'btn-primary' : 'btn-outline-primary');
}
document.getElementById('design_multiplier').addEventListener('input', function() {
updateDMLabel(this.value);
recalc();
});
// ── Marge ─────────────────────────────────────────────────────────────────────
function updateMarginDisplay(val) {
document.getElementById('marginDisplay').textContent = parseFloat(val).toFixed(1) + ' %';
}
const marginSlider = document.getElementById('gross_margin_pct');
marginSlider.addEventListener('input', function() { updateMarginDisplay(this.value); recalc(); });
marginSlider.addEventListener('change', function() { updateMarginDisplay(this.value); recalc(); });
// ── Profil tarif client ───────────────────────────────────────────────────────
document.getElementById('pricingProfileSelect').addEventListener('change', function() {
const opt = this.options[this.selectedIndex];
if (opt.value) {
const dm = parseFloat(opt.dataset.dm);
const margin = parseFloat(opt.dataset.margin);
if (!isNaN(dm)) {
document.getElementById('design_multiplier').value = dm;
updateDMLabel(dm);
}
if (!isNaN(margin)) {
marginSlider.value = margin;
updateMarginDisplay(margin);
}
recalc();
}
});
// ── Client : auto-remplir DM ──────────────────────────────────────────────────
document.getElementById('clientSelect').addEventListener('change', function() {
const dm = this.options[this.selectedIndex].dataset.dm;
if (dm) {
document.getElementById('design_multiplier').value = parseFloat(dm);
updateDMLabel(parseFloat(dm));
recalc();
}
});
// ── Matiere : afficher stock ──────────────────────────────────────────────────
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 = ''; }
else 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();
});
// ── Autres champs → recalc ────────────────────────────────────────────────────
['weight_g','hours','minutes','discount_pct'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', recalc);
});
['machineProfileSelect','handlingProfileSelect','materialProfileSelect'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('change', recalc);
});
// ── Calcul API ────────────────────────────────────────────────────────────────
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;
if (w <= 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,
machine_profile_id: document.getElementById('machineProfileSelect').value || null,
handling_profile_id: document.getElementById('handlingProfileSelect').value || null,
material_profile_id: document.getElementById('materialProfileSelect').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))
.catch(() => {});
}
// ── Rendu du recap ────────────────────────────────────────────────────────────
function fmt(n) { return parseFloat(n).toFixed(4) + ' €'; }
function fmt2(n) { return parseFloat(n).toFixed(2) + ' €'; }
function renderBreakdown(d, body) {
const dm = parseFloat(body.design_multiplier).toFixed(2);
const mgn = parseFloat(body.gross_margin_pct).toFixed(1);
// Nom du materiau selectionne
const matSel = document.getElementById('materialSelect');
const matOpt = matSel.options[matSel.selectedIndex];
const matName = matOpt.value ? (matOpt.dataset.name || matOpt.text.split('—')[0].trim()) : 'global';
// Remise
const dis = body.discount_pct > 0
? `<div class="breakdown-row text-danger"><span class="ps-2">Remise (${body.discount_pct}%)</span><span>-${fmt2(d.prix_ttc * body.discount_pct / 100)}</span></div>` : '';
// TVA — toujours affichee
const tvaLabel = d._tva_pct > 0
? `TVA ${d._tva_pct}% — ${fmt2(d.tva_amount)}`
: `TVA — franchise en base`;
const tvaClass = d._tva_pct > 0 ? '' : 'text-muted fst-italic';
// VFL
const vflRow = d._vfl_pct > 0
? `<div class="breakdown-row"><span class="breakdown-muted ps-2">VFL impot (${d._vfl_pct}%)</span><span>${fmt2(d.vfl_amount)}</span></div>` : '';
document.getElementById('breakdown').innerHTML = `
<div class="breakdown-row section-header"><span>Cout fixe</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Matiere <small class="text-secondary">${matName}</small></span><span>${fmt(d.material_cost)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge matiere (${d._material_margin_pct}%)</span><span>${fmt(d.material_margin)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure — imprimante</span><span>${fmt(d.printer_wear)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure — buse</span><span>${fmt(d.nozzle_wear)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure — plateau</span><span>${fmt(d.plate_wear)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Electricite <small class="text-secondary">${parseFloat(d.kwh_used).toFixed(3)} kWh × ${d._elec_price} €</small></span><span>${fmt(d.electricity_cost)}</span></div>
<div class="breakdown-row subtotal"><span class="fw-semibold">Total cout fixe</span><span class="fw-semibold">${fmt2(d.cout_fixe)}</span></div>
<div class="breakdown-row section-header"><span>Partie variable</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Manutention <small class="text-secondary">${d._handling_minutes} min × ${d._handling_rate} €/h</small></span><span>${fmt(d.handling_cost)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Design (x${dm})</span><span>${fmt(d.design_cost)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge brute (${mgn}%)</span><span>${fmt2(d.margin_amount)}</span></div>
<div class="breakdown-row subtotal"><span class="fw-semibold">Total marge</span><span class="fw-semibold">${fmt2(d.total_marge)}</span></div>
<div class="breakdown-row section-header"><span>Fiscal</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Cotisations BIC vente (${d._cot_pct}%)</span><span>${fmt2(d.cotisations_amount)}</span></div>
${vflRow}
<div class="breakdown-row ${tvaClass}"><span class="ps-2">${tvaLabel}</span></div>
${dis}
<div class="breakdown-row total"><span>PRIX FINAL TTC</span><span class="final-price">${fmt2(d.final_price)}</span></div>`;
}
// Init
updateDMLabel(1.80);
updateMarginDisplay(27.5);
</script>
{% endblock %}