From 0619559190b9005dfa89db2fdcd33674485a269b Mon Sep 17 00:00:00 2001 From: Jo Date: Thu, 16 Jul 2026 14:54:13 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Meshy.ai=20=E2=80=94=20raw=20cost=20in?= =?UTF-8?q?=20cout=5Ffixe,=20margin=20in=20variable,=20per-job=20margin=20?= =?UTF-8?q?%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HANDOFF.md | 32 ++++++++++++++++++++-------- app.py | 45 ++++++++++++++++++++++----------------- templates/job_detail.html | 7 ++++-- templates/new_job.html | 10 +++++---- 4 files changed, 60 insertions(+), 34 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index c239197..91dccb0 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -61,6 +61,10 @@ Dernières migrations ajoutées (à la fin de la liste) : 'ALTER TABLE calendar_blocks ADD COLUMN date_end TEXT DEFAULT NULL' 'ALTER TABLE jobs ADD COLUMN marge_pct_on_ht REAL DEFAULT 0' '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' +'ALTER TABLE jobs ADD COLUMN meshy_margin_pct REAL DEFAULT 0' ``` --- @@ -99,6 +103,15 @@ Colonne `date_end` optionnelle. Requêtes avec `COALESCE(date_end, date)` pour c - À la création d'un job depuis Nextcloud, le path est sauvegardé dans un hidden input - Au chargement de `new_job` avec `clone_from`, `autoParseOnClone()` appelle `/api/nextcloud/parse?path=...` et reconstitue le sélecteur de plateaux +### Intégration Meshy.ai +- Abonnement annuel + crédits mensuels configurables dans Paramètres (`meshy_annual_cost`, `meshy_monthly_credits`, `meshy_default_margin_pct`) +- Coût brut : `meshy_raw = credits × (annual_cost / 12 / monthly_credits)` → va dans **cout_fixe** (comme l'électricité) +- Marge Meshy : `meshy_margin = meshy_raw × meshy_margin_pct / 100` → va dans **total_marge** (partie variable) +- La `gross_margin_pct` s'applique aussi sur `cout_fixe` qui contient `meshy_raw` : double levier intentionnel +- Champ `meshy_margin_pct` saisi par job (défaut = `meshy_default_margin_pct` dans settings) +- Colonne `meshy_cost` en DB = coût brut ; `meshy_margin_amount` = calculé à la volée (`meshy_cost × meshy_margin_pct / 100`) +- Dans `job_detail.html` : `meshy_cost` affiché dans section Cout fixe, `meshy_margin_amount` dans Partie variable + --- ## Bugs résolus dans cette session @@ -127,7 +140,7 @@ Colonne `date_end` optionnelle. Requêtes avec `COALESCE(date_end, date)` pour c ### À surveiller - **jobs.html** : toujours utiliser le script Python pour modifier, vérifier la syntaxe Jinja2 après chaque modif avec `python3 -c "import jinja2; ..."` - **app.py** : après ajout de docstrings avec des accents, vérifier `python3 -m py_compile app.py` — les chaînes avec apostrophes dans les docstrings `"""` sont valides mais certaines substitutions de texte peuvent créer des conflits -- **INSERT jobs** : 38 colonnes = 38 `?`. Si on ajoute une colonne, ajouter aussi le `?` ET la valeur dans le tuple +- **INSERT jobs** : 42 colonnes = 42 `?`. Si on ajoute une colonne, ajouter aussi le `?` ET la valeur dans le tuple ### Fonctionnalité pas encore implémentée - Recalcul de `marge_pct_on_ht` pour les jobs existants (valeur = 0 pour les anciens jobs) @@ -171,14 +184,15 @@ git commit -m "description" Derniers commits : ``` -3da19f5 feat: auto-reparse Nextcloud 3mf on job clone -0069ab2 fix: INSERT jobs missing ? for marge_pct_on_ht -80307ac feat: plate_name and total_marge in jobs list -f51a789 fix: HA sync per-printer autocommit connections -522a0c5 fix: WAL mode SQLite -b55edce fix: skip capacity/overlap when already_done=true -7824124 fix: replace deprecated utcnow() -1899165 fix: gantt starts 12h before now +(à committer) feat: Meshy.ai — raw cost in cout_fixe, margin in variable, per-job margin % +3da19f5 feat: auto-reparse Nextcloud 3mf on job clone +0069ab2 fix: INSERT jobs missing ? for marge_pct_on_ht +80307ac feat: plate_name and total_marge in jobs list +f51a789 fix: HA sync per-printer autocommit connections +522a0c5 fix: WAL mode SQLite +b55edce fix: skip capacity/overlap when already_done=true +7824124 fix: replace deprecated utcnow() +1899165 fix: gantt starts 12h before now ``` --- diff --git a/app.py b/app.py index fba90a3..d0b9d9a 100644 --- a/app.py +++ b/app.py @@ -258,7 +258,7 @@ def init_db(): ('ha_poll_interval','60'), ('meshy_annual_cost','200.0'), ('meshy_monthly_credits','1000'), - ('meshy_cost_multiplier','1.0'), + ('meshy_default_margin_pct','30.0'), ] conn.executemany('INSERT OR IGNORE INTO settings VALUES (?,?)', defaults) @@ -308,6 +308,7 @@ def init_db(): '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', + 'ALTER TABLE jobs ADD COLUMN meshy_margin_pct REAL DEFAULT 0', ] for sql in migrations: try: @@ -463,7 +464,7 @@ def parse_3mf(file_stream): 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, - meshy_enabled=False, meshy_credits_used=0): + meshy_enabled=False, meshy_credits_used=0, meshy_margin_pct=0.0): """ Moteur de pricing principal — calcule le prix de vente d'une pièce. @@ -516,20 +517,21 @@ 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 + # Coût Meshy.ai — coût brut dans cout_fixe, marge spécifique dans total_marge + meshy_unit = 0.0 + meshy_raw = 0.0 + meshy_margin_amount = 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 + _meshy_annual = float(s.get('meshy_annual_cost', 200.0)) + _meshy_credits = float(s.get('meshy_monthly_credits', 1000)) or 1 + meshy_unit = _meshy_annual / 12 / _meshy_credits + meshy_raw = meshy_credits_used * meshy_unit + meshy_margin_amount = meshy_raw * (meshy_margin_pct / 100) - cout_fixe = mat + mat_margin + wear + elec - sous_total = cout_fixe + handling + design + meshy_billed + cout_fixe = mat + mat_margin + wear + elec + meshy_raw + sous_total = cout_fixe + handling + design margin = sous_total * (gross_margin_pct / 100) - total_marge= handling + design + meshy_billed + margin + total_marge= handling + design + meshy_margin_amount + margin prix_ht = cout_fixe + total_marge cot_rate = s.get('cotisations_rate_pct', 12.3) / 100 @@ -574,7 +576,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_cost': round(meshy_raw, 4), + 'meshy_margin_amount': round(meshy_margin_amount, 4), 'meshy_unit_cost': round(meshy_unit, 6), 'tax_amount': round(cot_amount + vfl_amount + other_taxes_amount, 4), 'final_price': round(final, 2), @@ -1332,22 +1335,24 @@ def new_job(): meshy_enabled_v = bool(request.form.get('meshy_enabled')) meshy_credits_v = max(0, int(request.form.get('meshy_credits_used', 0) or 0)) + meshy_margin_v = max(0.0, float(request.form.get('meshy_margin_pct', 0) or 0)) r = calc(weight_g, print_time_s, design_mult, gross_margin, discount, price_kg, s, pieces_per_plate, - meshy_enabled=meshy_enabled_v, meshy_credits_used=meshy_credits_v) + meshy_enabled=meshy_enabled_v, meshy_credits_used=meshy_credits_v, + meshy_margin_pct=meshy_margin_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, + meshy_enabled,meshy_credits_used,meshy_cost,meshy_margin_pct, 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'], + int(meshy_enabled_v),meshy_credits_v,r['meshy_cost'],meshy_margin_v, 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'], @@ -1713,6 +1718,7 @@ def api_calculate(): 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))) + meshy_margin_p = max(0.0, float(d.get('meshy_margin_pct', 0))) result = calc( weight_g=float(d['weight_g']), @@ -1722,7 +1728,8 @@ def api_calculate(): discount_pct=float(d.get('discount_pct',0)), price_per_kg_override=price_kg, s=s, pieces_per_plate=pieces_per_plate, - meshy_enabled=meshy_enabled, meshy_credits_used=meshy_credits) + meshy_enabled=meshy_enabled, meshy_credits_used=meshy_credits, + meshy_margin_pct=meshy_margin_p) # 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 e342fef..e52d15a 100644 --- a/templates/job_detail.html +++ b/templates/job_detail.html @@ -162,6 +162,9 @@ {{ brow('Marge matiere', job.material_margin, 'breakdown-muted ps-2', 4) }} {{ brow('Usure machine', job.wear_cost, 'breakdown-muted ps-2', 4) }} {{ brow('Electricite', job.electricity_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 %}
Total cout fixe {{ "%.2f"|format(cout_f) }} € @@ -172,8 +175,8 @@
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) }} + {% if job.meshy_enabled and job.meshy_cost and job.meshy_margin_pct %} + {{ brow('Marge Meshy.ai (' ~ job.meshy_margin_pct ~ '%)', job.meshy_cost * job.meshy_margin_pct / 100, 'breakdown-muted ps-2', 4) }} {% endif %} {{ brow('Marge brute (' ~ job.gross_margin_pct ~ '%)', job.margin_amount, 'breakdown-muted ps-2') }}
diff --git a/templates/new_job.html b/templates/new_job.html index 01a4602..52c0b5d 100644 --- a/templates/new_job.html +++ b/templates/new_job.html @@ -595,7 +595,7 @@ document.getElementById('materialSelect').addEventListener('change', function() }); // ── Autres champs → recalc ──────────────────────────────────────────────────── -['weight_g','hours','minutes','discount_pct','pieces_per_plate','order_qty','meshy_credits_used'].forEach(id => { +['weight_g','hours','minutes','discount_pct','pieces_per_plate','order_qty','meshy_credits_used','meshy_margin_pct'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('input', recalc); }); @@ -627,6 +627,7 @@ function recalc() { 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, + meshy_margin_pct: parseFloat(document.getElementById('meshy_margin_pct').value) || 0, }; fetch('/api/calculate', { @@ -637,9 +638,9 @@ function recalc() { .then(r => r.json()) .then(d => { renderBreakdown(d, body); - if (d.meshy_unit_cost) { + if (d.meshy_unit_cost && body.meshy_enabled) { document.getElementById('meshyCostHint').innerHTML = - `Coût unitaire : ${d.meshy_unit_cost.toFixed(4)} €/crédit`; + `${d.meshy_unit_cost.toFixed(4)} €/crédit · coût brut : ${d.meshy_cost.toFixed(2)} € · marge : ${d.meshy_margin_amount.toFixed(2)} €`; } }) .catch(() => {}); @@ -759,12 +760,13 @@ function renderBreakdown(d, body) { ${row(`Usure — buse`, d.nozzle_wear, '', true)} ${row(`Usure — plateau`, d.plate_wear, '', true)} ${row(`Electricite ${parseFloat(d.kwh_used).toFixed(3)} kWh × ${d._elec_price} €`, d.electricity_cost, '', true)} + ${body.meshy_enabled && d.meshy_cost > 0 ? row(`Meshy.ai — coût abonnement (${body.meshy_credits_used} crédits)`, d.meshy_cost, '', true) : ''} ${row(`Total cout fixe`, d.cout_fixe, 'subtotal')}
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) : ''} + ${body.meshy_enabled && d.meshy_margin_amount > 0 ? row(`Marge Meshy.ai (${body.meshy_margin_pct}%)`, d.meshy_margin_amount, '', true) : ''} ${row(`Marge brute (${mgn}%)`, d.margin_amount)} ${row(`Total marge (${d.marge_pct_on_ht}% du HT)`, d.total_marge, 'subtotal')}