feat: Meshy.ai — raw cost in cout_fixe, margin in variable, per-job margin %
Deploy via Portainer / deploy (push) Successful in 1s
Deploy via Portainer / deploy (push) Successful in 1s
This commit is contained in:
+15
-1
@@ -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,6 +184,7 @@ git commit -m "description"
|
||||
|
||||
Derniers commits :
|
||||
```
|
||||
(à 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
|
||||
|
||||
@@ -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
|
||||
# Coût Meshy.ai — coût brut dans cout_fixe, marge spécifique dans total_marge
|
||||
meshy_unit = 0.0
|
||||
meshy_billed = 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_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)
|
||||
|
||||
@@ -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 %}
|
||||
<div class="breakdown-row subtotal">
|
||||
<span class="flex-fill fw-semibold">Total cout fixe</span>
|
||||
<span class="text-end" style="min-width:85px">{{ "%.2f"|format(cout_f) }} €</span>
|
||||
@@ -172,8 +175,8 @@
|
||||
<div class="breakdown-row section-header"><span>Partie variable</span></div>
|
||||
{{ 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') }}
|
||||
<div class="breakdown-row subtotal">
|
||||
|
||||
@@ -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 =
|
||||
`<span class="text-muted">Coût unitaire : ${d.meshy_unit_cost.toFixed(4)} €/crédit</span>`;
|
||||
`<span class="text-muted">${d.meshy_unit_cost.toFixed(4)} €/crédit · coût brut : ${d.meshy_cost.toFixed(2)} € · marge : ${d.meshy_margin_amount.toFixed(2)} €</span>`;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -759,12 +760,13 @@ function renderBreakdown(d, body) {
|
||||
${row(`<span class="breakdown-muted ps-2">Usure — buse</span>`, d.nozzle_wear, '', true)}
|
||||
${row(`<span class="breakdown-muted ps-2">Usure — plateau</span>`, d.plate_wear, '', true)}
|
||||
${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)}
|
||||
${body.meshy_enabled && d.meshy_cost > 0 ? row(`<span class="breakdown-muted ps-2" style="color:#7c3aed"><i class="bi bi-stars me-1"></i>Meshy.ai — coût abonnement (${body.meshy_credits_used} crédits)</span>`, d.meshy_cost, '', true) : ''}
|
||||
${row(`<span class="fw-semibold">Total cout fixe</span>`, d.cout_fixe, 'subtotal')}
|
||||
|
||||
<div class="breakdown-row section-header"><span>Partie variable</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)}
|
||||
${row(`<span class="breakdown-muted ps-2">Design (x${dm})</span>`, d.design_cost, '', true)}
|
||||
${body.meshy_enabled && d.meshy_cost > 0 ? row(`<span class="breakdown-muted ps-2" style="color:#7c3aed"><i class="bi bi-stars me-1"></i>Meshy.ai (${body.meshy_credits_used} crédits)</span>`, d.meshy_cost, '', true) : ''}
|
||||
${body.meshy_enabled && d.meshy_margin_amount > 0 ? row(`<span class="breakdown-muted ps-2" style="color:#7c3aed"><i class="bi bi-stars me-1"></i>Marge Meshy.ai (${body.meshy_margin_pct}%)</span>`, d.meshy_margin_amount, '', true) : ''}
|
||||
${row(`<span class="breakdown-muted ps-2">Marge brute (${mgn}%)</span>`, d.margin_amount)}
|
||||
${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')}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user