Compare commits

...

2 Commits

Author SHA1 Message Date
Jo 0c9e04b6e7 fix: poids et temps sont ceux du plateau entier, divisés par pièces/plateau
Deploy via Portainer / deploy (push) Successful in 0s
2026-07-06 20:12:21 +02:00
Jo 614ea23a7c feat: colonnes prix/pièce et prix/commande dans les deux récaps 2026-07-06 20:12:14 +02:00
3 changed files with 199 additions and 118 deletions
+10 -7
View File
@@ -244,17 +244,20 @@ def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct,
if pieces_per_plate < 1: if pieces_per_plate < 1:
pieces_per_plate = 1 pieces_per_plate = 1
price_kg = price_per_kg_override if price_per_kg_override is not None else s['filament_price_per_kg'] # weight_g et print_time_s = valeurs PLATEAU complet (Bambu Studio)
mat = (price_kg / 1000) * weight_g # Tous les coûts sont ramenés à la pièce en divisant par pieces_per_plate
price_kg = price_per_kg_override if price_per_kg_override is not None else s['filament_price_per_kg']
weight_per_piece = weight_g / pieces_per_plate
mat = (price_kg / 1000) * weight_per_piece
mat_margin = mat * (s['material_margin_pct'] / 100) mat_margin = mat * (s['material_margin_pct'] / 100)
design = mat * design_mult design = mat * design_mult
# Manutention divisée par le nombre de pièces sur le plateau # Manutention, usure et élec divisées par pièces/plateau
handling = (s['handling_rate_per_hour'] / 60) * s['handling_minutes_per_plate'] / pieces_per_plate handling = (s['handling_rate_per_hour'] / 60) * s['handling_minutes_per_plate'] / pieces_per_plate
printer_w = (s['printer_price'] / (s['printer_lifespan_hours'] * 3600)) * print_time_s printer_w = (s['printer_price'] / (s['printer_lifespan_hours'] * 3600)) * print_time_s / pieces_per_plate
nozzle_w = (s['nozzle_price'] / (s['nozzle_lifespan_hours'] * 3600)) * print_time_s nozzle_w = (s['nozzle_price'] / (s['nozzle_lifespan_hours'] * 3600)) * print_time_s / pieces_per_plate
plate_w = (s['plate_price'] / (s['plate_lifespan_hours'] * 3600)) * print_time_s plate_w = (s['plate_price'] / (s['plate_lifespan_hours'] * 3600)) * print_time_s / pieces_per_plate
wear = printer_w + nozzle_w + plate_w wear = printer_w + nozzle_w + plate_w
elec = s['printer_power_kw'] * (print_time_s / 3600) * s['electricity_price_kwh'] elec = s['printer_power_kw'] * (print_time_s / 3600) * s['electricity_price_kwh'] / pieces_per_plate
cout_fixe = mat + mat_margin + wear + elec cout_fixe = mat + mat_margin + wear + elec
sous_total = cout_fixe + handling + design sous_total = cout_fixe + handling + design
+89 -49
View File
@@ -15,6 +15,9 @@
</form> </form>
</div> </div>
{% set qty = job.order_qty or 1 %}
{% set multi = qty > 1 %}
<div class="row g-4"> <div class="row g-4">
<div class="col-md-5"> <div class="col-md-5">
<div class="card mb-3"> <div class="card mb-3">
@@ -32,8 +35,10 @@
<tr><th class="text-muted fw-normal">Fichier source</th> <tr><th class="text-muted fw-normal">Fichier source</th>
<td class="small font-monospace">{{ job.source_file or '—' }}</td></tr> <td class="small font-monospace">{{ 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">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">Poids plateau</th><td>{{ job.weight_g }} g</td></tr>
<tr><th class="text-muted fw-normal">Duree impression</th><td>{{ job.print_time_s | fmt_time }}</td></tr> <tr><th class="text-muted fw-normal">Pieces / plateau</th><td>{{ job.pieces_per_plate or 1 }}</td></tr>
<tr><th class="text-muted fw-normal">Quantite commandee</th><td>{{ qty }} pièce{{ 's' if qty > 1 else '' }}</td></tr>
<tr><th class="text-muted fw-normal">Duree plateau</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">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">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> <tr><th class="text-muted fw-normal">Remise</th><td>{{ job.discount_pct }} %</td></tr>
@@ -57,91 +62,126 @@
<div class="col-md-7"> <div class="col-md-7">
<div class="card"> <div class="card">
<div class="card-header py-3"><i class="bi bi-receipt me-2"></i>Decomposition du prix</div> <div class="card-header py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-receipt me-2"></i>Decomposition du prix</span>
{% if multi %}
<span class="badge bg-secondary">{{ qty }} pièces commandées</span>
{% endif %}
</div>
<div class="card-body"> <div class="card-body">
<!-- Cout fixe --> {# Macro pour une ligne à 1 ou 2 colonnes #}
<div class="breakdown-row section-header"><span>Cout fixe</span></div> {% macro brow(label, val_piece, cls='', precision=2) %}
<div class="breakdown-row"><span class="breakdown-muted ps-2">Matiere{% if job.material_name %} <small class="text-secondary">{{ job.material_name }}</small>{% endif %}</span><span>{{ "%.4f"|format(job.material_cost) }} €</span></div> <div class="breakdown-row {{ cls }}">
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge matiere</span><span>{{ "%.4f"|format(job.material_margin) }}</span></div> <span class="flex-fill">{{ label }}</span>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure machine</span><span>{{ "%.4f"|format(job.wear_cost) }} €</span></div> <span class="text-end" style="min-width:85px">
<div class="breakdown-row"><span class="breakdown-muted ps-2">Electricite</span><span>{{ "%.4f"|format(job.electricity_cost) }} €</span></div> {% if precision == 4 %}{{ "%.4f"|format(val_piece) }} €
{% set cout_f = job.cout_fixe or (job.material_cost + job.material_margin + job.wear_cost + job.electricity_cost) %} {% else %}{{ "%.2f"|format(val_piece) }} €{% endif %}
<div class="breakdown-row subtotal"> </span>
<span class="fw-semibold">Total cout fixe</span> {% if multi %}
<span class="fw-semibold">{{ "%.2f"|format(cout_f) }} €</span> <span class="text-end text-muted" style="min-width:85px">
{% if precision == 4 %}{{ "%.2f"|format(val_piece * qty) }} €
{% else %}{{ "%.2f"|format(val_piece * qty) }} €{% endif %}
</span>
{% endif %}
</div> </div>
{% endmacro %}
<!-- Partie variable --> {# En-tête colonnes #}
<div class="breakdown-row section-header"><span>Partie variable</span></div> {% if multi %}
<div class="breakdown-row"><span class="breakdown-muted ps-2">Manutention</span><span>{{ "%.4f"|format(job.handling_cost) }} €</span></div> <div class="d-flex justify-content-end small text-muted mb-2 border-bottom pb-1 gap-0">
<div class="breakdown-row"><span class="breakdown-muted ps-2">Design (×{{ job.design_multiplier }})</span><span>{{ "%.4f"|format(job.design_cost) }} €</span></div> <span style="min-width:85px;text-align:right">/ pièce</span>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge brute ({{ job.gross_margin_pct }}%)</span><span>{{ "%.2f"|format(job.margin_amount) }}</span></div> <span style="min-width:85px;text-align:right">/ commande ×{{ qty }}</span>
</div>
{% endif %}
{% set cout_f = job.cout_fixe or (job.material_cost + job.material_margin + job.wear_cost + job.electricity_cost) %}
{% set tot_m = job.total_marge or (job.handling_cost + job.design_cost + job.margin_amount) %} {% set tot_m = job.total_marge or (job.handling_cost + job.design_cost + job.margin_amount) %}
{% set prix_ttc = job.final_price / (1 - job.discount_pct / 100) if job.discount_pct < 100 else job.final_price %} {% set prix_ttc = job.final_price / (1 - job.discount_pct / 100) if job.discount_pct < 100 else job.final_price %}
{% set prix_ht_net = prix_ttc - (job.tva_amount or 0) %} {% set prix_ht_net = prix_ttc - (job.tva_amount or 0) %}
{% set marge_pct_ht = (tot_m / prix_ht_net * 100)|round(1) if prix_ht_net > 0 else 0 %} {% set marge_pct_ht = (tot_m / prix_ht_net * 100)|round(1) if prix_ht_net > 0 else 0 %}
<div class="breakdown-row subtotal">
<span class="fw-semibold">Total marge <small class="text-muted fw-normal">({{ marge_pct_ht }}% du HT)</small></span> <!-- Cout fixe -->
<span class="fw-semibold">{{ "%.2f"|format(tot_m) }} €</span> <div class="breakdown-row section-header"><span>Cout fixe</span></div>
</div> {{ brow('Matiere' + (' <small class="text-secondary">' + job.material_name + '</small>' if job.material_name else ''), job.material_cost, '', 4) }}
{{ brow('Marge matiere', job.material_margin, '', 4) }}
{{ brow('Usure machine', job.wear_cost, '', 4) }}
{{ brow('Electricite', job.electricity_cost, '', 4) }}
{{ brow('<span class="fw-semibold">Total cout fixe</span>', cout_f, 'subtotal') }}
<!-- Partie variable -->
<div class="breakdown-row section-header"><span>Partie variable</span></div>
{{ brow('Manutention', job.handling_cost, '', 4) }}
{{ brow('Design (×' + job.design_multiplier|string + ')', job.design_cost, '', 4) }}
{{ brow('Marge brute (' + job.gross_margin_pct|string + '%)', job.margin_amount) }}
{{ brow('<span class="fw-semibold">Total marge <small class="text-muted fw-normal">(' + marge_pct_ht|string + '% du HT)</small></span>', tot_m, 'subtotal') }}
<!-- Fiscal --> <!-- Fiscal -->
<div class="breakdown-row section-header"><span>Fiscal</span></div> <div class="breakdown-row section-header"><span>Fiscal</span></div>
<div class="breakdown-row"> {{ brow('Cotisations BIC vente', job.cotisations_amount or job.tax_amount or 0) }}
<span class="breakdown-muted ps-2">Cotisations BIC vente</span>
<span>{% if job.cotisations_amount %}{{ "%.2f"|format(job.cotisations_amount) }}{% else %}{{ "%.2f"|format(job.tax_amount or 0) }}{% endif %} €</span>
</div>
{% if job.vfl_amount and job.vfl_amount > 0 %} {% if job.vfl_amount and job.vfl_amount > 0 %}
<div class="breakdown-row"> {{ brow('VFL impot', job.vfl_amount) }}
<span class="breakdown-muted ps-2">VFL impot</span>
<span>{{ "%.2f"|format(job.vfl_amount) }} €</span>
</div>
{% endif %} {% endif %}
{% if job.other_taxes_amount and job.other_taxes_amount > 0 %} {% if job.other_taxes_amount and job.other_taxes_amount > 0 %}
<div class="breakdown-row"> {{ brow('Autres taxes / CFE', job.other_taxes_amount) }}
<span class="breakdown-muted ps-2">Autres taxes / CFE</span>
<span>{{ "%.2f"|format(job.other_taxes_amount) }} €</span>
</div>
{% endif %} {% endif %}
<div class="breakdown-row total" style="border-top:none">
<span>Prix HT</span> <!-- Prix HT -->
<span class="final-price">{{ "%.2f"|format(prix_ht_net) }} €</span> <div class="breakdown-row" style="border-top:none">
<span class="fw-bold flex-fill">Prix HT</span>
<span class="final-price text-end" style="font-size:1.3rem;min-width:85px">{{ "%.2f"|format(prix_ht_net) }} €</span>
{% if multi %}<span class="final-price text-end" style="font-size:1.3rem;min-width:85px;opacity:.75">{{ "%.2f"|format(prix_ht_net * qty) }} €</span>{% endif %}
</div> </div>
{% if job.tva_amount and job.tva_amount > 0 %} {% if job.tva_amount and job.tva_amount > 0 %}
<div class="breakdown-row"> <div class="breakdown-row">
<span class="ps-2">TVA</span> <span class="ps-2 flex-fill">TVA</span>
<span>{{ "%.2f"|format(job.tva_amount) }} €</span> <span class="text-end" style="min-width:85px">{{ "%.2f"|format(job.tva_amount) }} €</span>
{% if multi %}<span class="text-end text-muted" style="min-width:85px">{{ "%.2f"|format(job.tva_amount * qty) }} €</span>{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if job.discount_pct > 0 %} {% if job.discount_pct > 0 %}
<div class="breakdown-row text-danger"> <div class="breakdown-row text-danger">
<span class="ps-2">Remise ({{ job.discount_pct }}%)</span> <span class="ps-2 flex-fill">Remise ({{ job.discount_pct }}%)</span>
<span>{{ "%.2f"|format(prix_ttc * job.discount_pct / 100) }} €</span> <span class="text-end" style="min-width:85px">{{ "%.2f"|format(prix_ttc * job.discount_pct / 100) }} €</span>
{% if multi %}<span class="text-end" style="min-width:85px">{{ "%.2f"|format(prix_ttc * qty * job.discount_pct / 100) }} €</span>{% endif %}
</div> </div>
{% endif %} {% endif %}
<!-- Prix final -->
{% if multi %}
<div class="d-flex justify-content-between align-items-center mt-2 pt-2" style="border-top:2px solid #dee2e6">
<span class="fw-bold">PRIX FINAL TTC</span>
<div class="d-flex gap-3">
<span class="final-price" style="font-size:1.4rem">{{ "%.2f"|format(job.final_price) }} €</span>
<span class="final-price" style="font-size:1.4rem;opacity:.7">× {{ qty }} = {{ "%.2f"|format(job.final_price * qty) }} €</span>
</div>
</div>
{% else %}
<div class="breakdown-row total"> <div class="breakdown-row total">
<span>PRIX FINAL TTC</span> <span>PRIX FINAL TTC</span>
<span class="final-price">{{ "%.2f"|format(job.final_price) }} €</span> <span class="final-price">{{ "%.2f"|format(job.final_price) }} €</span>
</div> </div>
{% endif %}
<!-- Barre de composition --> <!-- Barre de composition -->
{% set fiscal = (job.cotisations_amount or job.tax_amount or 0) + (job.vfl_amount or 0) + (job.other_taxes_amount or 0) + (job.tva_amount or 0) %} {% set fiscal = (job.cotisations_amount or job.tax_amount or 0) + (job.vfl_amount or 0) + (job.other_taxes_amount or 0) + (job.tva_amount or 0) %}
{% set total = job.final_price %} {% if job.final_price > 0 %}
{% if total > 0 %}
<div class="mt-4"> <div class="mt-4">
<div class="d-flex justify-content-between small text-muted mb-1"> <div class="d-flex justify-content-between small text-muted mb-1">
<span>Cout fixe ({{ (cout_f/total*100)|round(1) }}%)</span> <span>Cout fixe ({{ (cout_f/job.final_price*100)|round(1) }}%)</span>
<span>Marge ({{ (tot_m/total*100)|round(1) }}%)</span> <span>Marge ({{ (tot_m/job.final_price*100)|round(1) }}%)</span>
<span>Fiscal ({{ (fiscal/total*100)|round(1) }}%)</span> <span>Fiscal ({{ (fiscal/job.final_price*100)|round(1) }}%)</span>
</div> </div>
<div class="progress" style="height:18px;border-radius:6px"> <div class="progress" style="height:18px;border-radius:6px">
<div class="progress-bar bg-secondary" style="width:{{ (cout_f/total*100)|round(1) }}%"></div> <div class="progress-bar bg-secondary" style="width:{{ (cout_f/job.final_price*100)|round(1) }}%"></div>
<div class="progress-bar" style="width:{{ (tot_m/total*100)|round(1) }}%;background:#ff6b35"></div> <div class="progress-bar" style="width:{{ (tot_m/job.final_price*100)|round(1) }}%;background:#ff6b35"></div>
<div class="progress-bar bg-warning text-dark" style="width:{{ (fiscal/total*100)|round(1) }}%"></div> <div class="progress-bar bg-warning text-dark" style="width:{{ (fiscal/job.final_price*100)|round(1) }}%"></div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
+100 -62
View File
@@ -73,10 +73,10 @@
</div> </div>
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Poids filament (g)</label> <label class="form-label fw-semibold">Poids plateau (g)</label>
<input type="number" name="weight_g" id="weight_g" class="form-control" <input type="number" name="weight_g" id="weight_g" class="form-control"
step="0.01" min="0.1" required placeholder="423.97"> step="0.01" min="0.1" required placeholder="423.97">
<div class="form-text">Champ Modele dans Bambu</div> <div class="form-text">Poids total du plateau Bambu (toutes pièces)</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Heures</label> <label class="form-label fw-semibold">Heures</label>
@@ -417,59 +417,77 @@ function fmt2(n) { return parseFloat(n).toFixed(2) + ' €'; }
function renderBreakdown(d, body) { function renderBreakdown(d, body) {
const dm = parseFloat(body.design_multiplier).toFixed(2); const dm = parseFloat(body.design_multiplier).toFixed(2);
const mgn = parseFloat(body.gross_margin_pct).toFixed(1); const mgn = parseFloat(body.gross_margin_pct).toFixed(1);
const qty = body.order_qty || 1;
const ppp = body.pieces_per_plate || 1;
const multi = qty > 1;
// Nom du materiau selectionne // Nom du materiau
const matSel = document.getElementById('materialSelect'); const matSel = document.getElementById('materialSelect');
const matOpt = matSel.options[matSel.selectedIndex]; const matOpt = matSel.options[matSel.selectedIndex];
const matName = matOpt.value ? (matOpt.dataset.name || matOpt.text.split('—')[0].trim()) : 'global'; const matName = matOpt.value ? (matOpt.dataset.name || matOpt.text.split('—')[0].trim()) : 'global';
// TVA // TVA
const tvaLabel = d._tva_pct > 0 ? `TVA (${d._tva_pct}%)` : `TVA — franchise en base`; const tvaLabel = d._tva_pct > 0 ? `TVA (${d._tva_pct}%)` : `TVA — franchise en base`;
const tvaClass = d._tva_pct > 0 ? '' : 'text-muted fst-italic'; const tvaClass = d._tva_pct > 0 ? '' : 'text-muted fst-italic';
// VFL // Helper : génère une ligne avec 1 ou 2 colonnes de valeurs
const vflRow = d._vfl_pct > 0 // cls : classes CSS supplémentaires sur le div
? `<div class="breakdown-row"><span class="breakdown-muted ps-2">VFL impot (${d._vfl_pct}%)</span><span>${fmt2(d.vfl_amount)}</span></div>` : ''; function row(label, pieceVal, extraCls = '', isFmt4 = false) {
const fmtFn = isFmt4 ? fmt : fmt2;
// Autres taxes const p = fmtFn(pieceVal);
const otherRow = d._other_taxes_pct > 0 const o = multi ? `<span class="text-end" style="min-width:80px">${fmt2(pieceVal * qty)}</span>` : '';
? `<div class="breakdown-row"><span class="breakdown-muted ps-2">Autres taxes / CFE (${d._other_taxes_pct}%)</span><span>${fmt2(d.other_taxes_amount)}</span></div>` : ''; return `<div class="breakdown-row${extraCls ? ' '+extraCls : ''}">
<span class="flex-fill">${label}</span>
// Remise <span class="text-end" style="min-width:80px">${p}</span>
const disRow = body.discount_pct > 0 ${o}
? `<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>` : ''; </div>`;
}
// Multi-piece header function rowFiscal(label, pieceVal, extraCls = '') {
const qty = body.order_qty || 1; const p = fmt2(pieceVal);
const ppp = body.pieces_per_plate || 1; const o = multi ? `<span class="text-end" style="min-width:80px">${fmt2(pieceVal * qty)}</span>` : '';
const nbPlateaux = Math.ceil(qty / ppp); return `<div class="breakdown-row${extraCls ? ' '+extraCls : ''}">
const lastPlate = qty % ppp; <span class="flex-fill">${label}</span>
const priceOrder = d.price_order || (d.final_price * qty); <span class="text-end" style="min-width:80px">${p}</span>
let plateauAlert = ''; ${o}
if (lastPlate !== 0) {
plateauAlert = `<div class="alert alert-warning py-2 mb-2 small">
<i class="bi bi-exclamation-triangle me-1"></i>
Dernier plateau incomplet : <strong>${lastPlate}/${ppp} pièces</strong>.
Vous pourriez imprimer jusqu'à <strong>${nbPlateaux * ppp}</strong> pièces pour le même coût fixe.
</div>`; </div>`;
} }
// Prix header (1 ou 2 colonnes) // Alerte plateau incomplet
const multiPriceHeader = qty > 1 const nbPlateaux = Math.ceil(qty / ppp);
? `<div class="d-flex justify-content-between align-items-center mb-3"> const lastPlate = qty % ppp;
<div class="text-center flex-fill border-end"> let plateauAlert = '';
<div class="small text-muted">Prix / pièce</div> if (lastPlate !== 0) {
<div class="fw-bold fs-5" style="color:#ff6b35">${fmt2(d.final_price)}</div> plateauAlert = `<div class="alert alert-warning py-2 mb-2 small">
</div> <i class="bi bi-exclamation-triangle me-1"></i>
<div class="text-center flex-fill"> Dernier plateau incomplet : <strong>${lastPlate}/${ppp} pièces</strong> —
<div class="small text-muted">Prix commande (×${qty})</div> vous pourriez imprimer <strong>${nbPlateaux * ppp}</strong> pièces pour le même coût fixe.
<div class="fw-bold fs-5" style="color:#ff6b35">${fmt2(priceOrder)}</div> </div>`;
</div> }
</div>`
: ''; // En-tête colonnes (multi uniquement)
const colHeader = multi
? `<div class="d-flex justify-content-end gap-0 small text-muted mb-1 border-bottom pb-1">
<span style="min-width:80px;text-align:right">/ pièce</span>
<span style="min-width:80px;text-align:right">/ commande ×${qty}</span>
</div>` : '';
// VFL / autres taxes
const vflRow = d._vfl_pct > 0 ? rowFiscal(`<span class="breakdown-muted ps-2">VFL impot (${d._vfl_pct}%)</span>`, d.vfl_amount) : '';
const otherRow = d._other_taxes_pct > 0 ? rowFiscal(`<span class="breakdown-muted ps-2">Autres taxes / CFE (${d._other_taxes_pct}%)</span>`, d.other_taxes_amount) : '';
// Remise (sur prix TTC/pièce)
let disRow = '';
if (body.discount_pct > 0) {
const remisePiece = d.prix_ttc * body.discount_pct / 100;
const o = multi ? `<span class="text-end text-danger" style="min-width:80px">-${fmt2(remisePiece * qty)}</span>` : '';
disRow = `<div class="breakdown-row text-danger">
<span class="flex-fill">Remise (${body.discount_pct}%)</span>
<span class="text-end" style="min-width:80px">-${fmt2(remisePiece)}</span>${o}
</div>`;
}
// Capacité // Capacité
const cap = d.capacity || {}; const cap = d.capacity || {};
const totalMin = d.total_print_min || 0; const totalMin = d.total_print_min || 0;
const capBlock = ` const capBlock = `
<div class="breakdown-row section-header mt-1"><span>Capacite de production</span></div> <div class="breakdown-row section-header mt-1"><span>Capacite de production</span></div>
@@ -479,31 +497,51 @@ function renderBreakdown(d, body) {
<div class="breakdown-row"><span class="breakdown-muted ps-2">Pieces / semaine</span><span>${cap.pieces_per_week||'—'}</span></div> <div class="breakdown-row"><span class="breakdown-muted ps-2">Pieces / semaine</span><span>${cap.pieces_per_week||'—'}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Pieces / mois (30j)</span><span>${cap.pieces_per_month||'—'}</span></div>`; <div class="breakdown-row"><span class="breakdown-muted ps-2">Pieces / mois (30j)</span><span>${cap.pieces_per_month||'—'}</span></div>`;
document.getElementById('breakdown').innerHTML = plateauAlert + multiPriceHeader + ` // Prix finaux
const pricePiece = d.final_price;
const priceOrder = pricePiece * qty;
const finalLabel = multi
? `<div class="d-flex justify-content-between align-items-center mt-2 pt-2" style="border-top:2px solid #dee2e6">
<span class="fw-bold">PRIX FINAL TTC</span>
<div class="d-flex gap-2">
<span class="final-price" style="font-size:1.4rem">${fmt2(pricePiece)}</span>
<span class="final-price" style="font-size:1.4rem;opacity:.7">× ${qty} = ${fmt2(priceOrder)}</span>
</div>
</div>`
: `<div class="breakdown-row total"><span>PRIX FINAL TTC</span><span class="final-price">${fmt2(pricePiece)}</span></div>`;
document.getElementById('breakdown').innerHTML = plateauAlert + colHeader + `
<div class="breakdown-row section-header"><span>Cout fixe</span></div> <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> ${row(`<span class="breakdown-muted ps-2">Matiere <small class="text-secondary">${matName}</small></span>`, d.material_cost, '', true)}
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge matiere (${d._material_margin_pct}%)</span><span>${fmt(d.material_margin)}</span></div> ${row(`<span class="breakdown-muted ps-2">Marge matiere (${d._material_margin_pct}%)</span>`, d.material_margin, '', true)}
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure — imprimante</span><span>${fmt(d.printer_wear)}</span></div> ${row(`<span class="breakdown-muted ps-2">Usure — imprimante</span>`, d.printer_wear, '', true)}
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure — buse</span><span>${fmt(d.nozzle_wear)}</span></div> ${row(`<span class="breakdown-muted ps-2">Usure — buse</span>`, d.nozzle_wear, '', true)}
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure — plateau</span><span>${fmt(d.plate_wear)}</span></div> ${row(`<span class="breakdown-muted ps-2">Usure — plateau</span>`, d.plate_wear, '', true)}
<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> ${row(`<span class="breakdown-muted ps-2">Electricite <small class="text-secondary">${parseFloat(d.kwh_used).toFixed(3)} kWh × ${d._elec_price} €</small></span>`, d.electricity_cost, '', true)}
<div class="breakdown-row subtotal"><span class="fw-semibold">Total cout fixe</span><span class="fw-semibold">${fmt2(d.cout_fixe)}</span></div> ${row(`<span class="fw-semibold">Total cout fixe</span>`, d.cout_fixe, 'subtotal')}
<div class="breakdown-row section-header"><span>Partie variable</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> ${row(`<span class="breakdown-muted ps-2">Manutention <small class="text-secondary">${d._handling_minutes} min × ${d._handling_rate} €/h</small></span>`, d.handling_cost, '', true)}
<div class="breakdown-row"><span class="breakdown-muted ps-2">Design (x${dm})</span><span>${fmt(d.design_cost)}</span></div> ${row(`<span class="breakdown-muted ps-2">Design (x${dm})</span>`, d.design_cost, '', true)}
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge brute (${mgn}%)</span><span>${fmt2(d.margin_amount)}</span></div> ${row(`<span class="breakdown-muted ps-2">Marge brute (${mgn}%)</span>`, d.margin_amount)}
<div class="breakdown-row subtotal"><span class="fw-semibold">Total marge <small class="text-muted fw-normal">(${d.marge_pct_on_ht}% du HT)</small></span><span class="fw-semibold">${fmt2(d.total_marge)}</span></div> ${row(`<span class="fw-semibold">Total marge <small class="text-muted fw-normal">(${d.marge_pct_on_ht}% du HT)</small></span>`, d.total_marge, 'subtotal')}
<div class="breakdown-row section-header"><span>Fiscal</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> ${rowFiscal(`<span class="breakdown-muted ps-2">Cotisations BIC vente (${d._cot_pct}%)</span>`, d.cotisations_amount)}
${vflRow} ${vflRow}${otherRow}
${otherRow} <div class="breakdown-row" style="border-top:none">
<div class="breakdown-row total" style="border-top:none"><span>Prix HT</span><span class="final-price">${fmt2(d.prix_ht_net)}</span></div> <span class="fw-bold">Prix HT</span>
<div class="breakdown-row ${tvaClass}"><span class="ps-2">${tvaLabel}</span><span>${d._tva_pct > 0 ? fmt2(d.tva_amount) : ''}</span></div> <span class="final-price" style="font-size:1.3rem;text-align:right;min-width:80px">${fmt2(d.prix_ht_net)}</span>
${multi ? `<span class="final-price" style="font-size:1.3rem;text-align:right;min-width:80px;opacity:.75">${fmt2(d.prix_ht_net * qty)}</span>` : ''}
</div>
<div class="breakdown-row ${tvaClass}">
<span class="ps-2">${tvaLabel}</span>
<span class="text-end" style="min-width:80px">${d._tva_pct > 0 ? fmt2(d.tva_amount) : ''}</span>
${multi && d._tva_pct > 0 ? `<span class="text-end" style="min-width:80px">${fmt2(d.tva_amount * qty)}</span>` : (multi ? '<span style="min-width:80px"></span>' : '')}
</div>
${disRow} ${disRow}
<div class="breakdown-row total"><span>PRIX FINAL TTC</span><span class="final-price">${fmt2(d.final_price)}</span></div> ${finalLabel}
` + capBlock; ` + capBlock;
} }
// Init // Init