diff --git a/app.py b/app.py index 448164d..fba90a3 100644 --- a/app.py +++ b/app.py @@ -256,6 +256,9 @@ def init_db(): ('ha_url',''), ('ha_token',''), ('ha_poll_interval','60'), + ('meshy_annual_cost','200.0'), + ('meshy_monthly_credits','1000'), + ('meshy_cost_multiplier','1.0'), ] conn.executemany('INSERT OR IGNORE INTO settings VALUES (?,?)', defaults) @@ -302,6 +305,9 @@ def init_db(): 'ALTER TABLE print_slots ADD COLUMN pieces_override INTEGER DEFAULT NULL', 'ALTER TABLE calendar_blocks ADD COLUMN date_end TEXT DEFAULT NULL', 'ALTER TABLE jobs ADD COLUMN source_nc_path TEXT DEFAULT ""', + 'ALTER TABLE jobs ADD COLUMN meshy_enabled INTEGER DEFAULT 0', + 'ALTER TABLE jobs ADD COLUMN meshy_credits_used INTEGER DEFAULT 0', + 'ALTER TABLE jobs ADD COLUMN meshy_cost REAL DEFAULT 0', ] for sql in migrations: try: @@ -456,7 +462,8 @@ def parse_3mf(file_stream): return result def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct, - price_per_kg_override=None, s=None, pieces_per_plate=1): + price_per_kg_override=None, s=None, pieces_per_plate=1, + meshy_enabled=False, meshy_credits_used=0): """ Moteur de pricing principal — calcule le prix de vente d'une pièce. @@ -509,10 +516,20 @@ def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct, wear = printer_w + nozzle_w + plate_w elec = s['printer_power_kw'] * (print_time_s / 3600) * s['electricity_price_kwh'] / pieces_per_plate + # Coût Meshy.ai (génération IA) — pro-raté par crédits utilisés + meshy_unit = 0.0 + meshy_billed = 0.0 + if meshy_enabled and meshy_credits_used > 0: + _meshy_annual = float(s.get('meshy_annual_cost', 200.0)) + _meshy_credits = float(s.get('meshy_monthly_credits', 1000)) or 1 + _meshy_mult = float(s.get('meshy_cost_multiplier', 1.0)) + meshy_unit = _meshy_annual / 12 / _meshy_credits + meshy_billed = meshy_credits_used * meshy_unit * _meshy_mult + cout_fixe = mat + mat_margin + wear + elec - sous_total = cout_fixe + handling + design + sous_total = cout_fixe + handling + design + meshy_billed margin = sous_total * (gross_margin_pct / 100) - total_marge= handling + design + margin + total_marge= handling + design + meshy_billed + margin prix_ht = cout_fixe + total_marge cot_rate = s.get('cotisations_rate_pct', 12.3) / 100 @@ -557,6 +574,8 @@ def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct, 'tva_amount': round(tva_amount, 4), 'prix_ht_net': round(prix_ht_net, 4), 'prix_ttc': round(prix_ttc, 4), + 'meshy_cost': round(meshy_billed, 4), + 'meshy_unit_cost': round(meshy_unit, 6), 'tax_amount': round(cot_amount + vfl_amount + other_taxes_amount, 4), 'final_price': round(final, 2), '_cot_pct': round(cot_rate * 100, 2), @@ -1311,19 +1330,24 @@ def new_job(): if mat: price_kg = mat['price_per_kg'] + meshy_enabled_v = bool(request.form.get('meshy_enabled')) + meshy_credits_v = max(0, int(request.form.get('meshy_credits_used', 0) or 0)) r = calc(weight_g, print_time_s, design_mult, gross_margin, discount, - price_kg, s, pieces_per_plate) + price_kg, s, pieces_per_plate, + meshy_enabled=meshy_enabled_v, meshy_credits_used=meshy_credits_v) conn.execute('''INSERT INTO jobs(client_id,material_id,machine_profile_id,handling_profile_id, material_profile_id,pricing_profile_id,project_id,source_file,source_nc_path,plate_name,name,description, + meshy_enabled,meshy_credits_used,meshy_cost, weight_g,print_time_s,design_multiplier,gross_margin_pct,discount_pct, pieces_per_plate,order_qty, material_cost,material_margin,design_cost,handling_cost,wear_cost,electricity_cost, cout_fixe,total_marge,subtotal,margin_amount,marge_pct_on_ht, cotisations_amount,vfl_amount,tva_amount,other_taxes_amount,tax_amount, price_per_piece,final_price,notes) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (client_id,material_id,machine_pid,handling_pid,material_pid,pricing_pid,project_id, source_file,source_nc_path,plate_name,request.form['name'],request.form.get('description',''), + int(meshy_enabled_v),meshy_credits_v,r['meshy_cost'], weight_g,print_time_s,design_mult,gross_margin,discount, pieces_per_plate,order_qty, r['material_cost'],r['material_margin'],r['design_cost'],r['handling_cost'], @@ -1687,6 +1711,8 @@ def api_calculate(): pieces_per_plate = max(1, int(d.get('pieces_per_plate', 1))) order_qty = max(1, int(d.get('order_qty', 1))) print_time_s = int(d.get('hours',0))*3600 + int(d.get('minutes',0))*60 + meshy_enabled = bool(d.get('meshy_enabled', False)) + meshy_credits = max(0, int(d.get('meshy_credits_used', 0))) result = calc( weight_g=float(d['weight_g']), @@ -1695,7 +1721,8 @@ def api_calculate(): gross_margin_pct=float(d.get('gross_margin_pct',27.5)), discount_pct=float(d.get('discount_pct',0)), price_per_kg_override=price_kg, s=s, - pieces_per_plate=pieces_per_plate) + pieces_per_plate=pieces_per_plate, + meshy_enabled=meshy_enabled, meshy_credits_used=meshy_credits) # Multi-piece extras nb_plateaux = math.ceil(order_qty / pieces_per_plate) diff --git a/templates/job_detail.html b/templates/job_detail.html index 7326eeb..e342fef 100644 --- a/templates/job_detail.html +++ b/templates/job_detail.html @@ -172,6 +172,9 @@
Partie variable
{{ brow('Manutention', job.handling_cost, 'breakdown-muted ps-2', 4) }} {{ brow('Design (×' ~ job.design_multiplier ~ ')', job.design_cost, 'breakdown-muted ps-2', 4) }} + {% if job.meshy_enabled and job.meshy_cost %} + {{ brow('Meshy.ai (' ~ job.meshy_credits_used ~ ' crédits)', job.meshy_cost, 'breakdown-muted ps-2', 4) }} + {% endif %} {{ brow('Marge brute (' ~ job.gross_margin_pct ~ '%)', job.margin_amount, 'breakdown-muted ps-2') }}
Total marge ({{ marge_pct_ht }}% du HT) diff --git a/templates/new_job.html b/templates/new_job.html index fece566..01a4602 100644 --- a/templates/new_job.html +++ b/templates/new_job.html @@ -285,6 +285,27 @@
+
+
+ + +
+
+ +
+ + crédits +
+
+
+
@@ -574,7 +595,7 @@ document.getElementById('materialSelect').addEventListener('change', function() }); // ── Autres champs → recalc ──────────────────────────────────────────────────── -['weight_g','hours','minutes','discount_pct','pieces_per_plate','order_qty'].forEach(id => { +['weight_g','hours','minutes','discount_pct','pieces_per_plate','order_qty','meshy_credits_used'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('input', recalc); }); @@ -604,6 +625,8 @@ function recalc() { material_profile_id: document.getElementById('materialProfileSelect').value || null, pieces_per_plate: parseInt(document.getElementById('pieces_per_plate').value) || 1, order_qty: parseInt(document.getElementById('order_qty').value) || 1, + meshy_enabled: document.getElementById('meshy_enabled').checked, + meshy_credits_used: parseInt(document.getElementById('meshy_credits_used').value) || 0, }; fetch('/api/calculate', { @@ -612,7 +635,13 @@ function recalc() { body: JSON.stringify(body) }) .then(r => r.json()) - .then(d => renderBreakdown(d, body)) + .then(d => { + renderBreakdown(d, body); + if (d.meshy_unit_cost) { + document.getElementById('meshyCostHint').innerHTML = + `Coût unitaire : ${d.meshy_unit_cost.toFixed(4)} €/crédit`; + } + }) .catch(() => {}); } @@ -620,6 +649,12 @@ function recalc() { function fmt(n) { return parseFloat(n).toFixed(4) + ' €'; } function fmt2(n) { return parseFloat(n).toFixed(2) + ' €'; } +function onMeshyToggle() { + const checked = document.getElementById('meshy_enabled').checked; + document.getElementById('meshyCreditsRow').classList.toggle('d-none', !checked); + recalc(); +} + function renderBreakdown(d, body) { const dm = parseFloat(body.design_multiplier).toFixed(2); const mgn = parseFloat(body.gross_margin_pct).toFixed(1); @@ -729,6 +764,7 @@ function renderBreakdown(d, body) {
Partie variable
${row(`Manutention ${d._handling_minutes} min × ${d._handling_rate} €/h`, d.handling_cost, '', true)} ${row(`Design (x${dm})`, d.design_cost, '', true)} + ${body.meshy_enabled && d.meshy_cost > 0 ? row(`Meshy.ai (${body.meshy_credits_used} crédits)`, d.meshy_cost, '', true) : ''} ${row(`Marge brute (${mgn}%)`, d.margin_amount)} ${row(`Total marge (${d.marge_pct_on_ht}% du HT)`, d.total_marge, 'subtotal')} diff --git a/templates/settings.html b/templates/settings.html index b35eca6..d86855d 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -286,6 +286,45 @@ + +
+
+
Meshy.ai — Génération 3D par IA
+
+
+
+ + +
Montant total payé par an (ex: 200 € pour Meshy Pro).
+
+
+ + +
Nb de crédits inclus chaque mois dans l'abonnement.
+
+
+ + +
+ ×1.0 = répercute le coût exact. ×1.5 = coût + 50% de marge sur Meshy. +
Coût unitaire actuel : + + {% set unit = settings.get('meshy_annual_cost', 200) / 12 / [settings.get('meshy_monthly_credits', 1000), 1]|max %} + {{ "%.4f"|format(unit) }} €/crédit + +
+
+
+
+
+
+