feat: Meshy.ai cost integration — settings, new job form, price breakdown
Deploy via Portainer / deploy (push) Successful in 0s
Deploy via Portainer / deploy (push) Successful in 0s
This commit is contained in:
@@ -256,6 +256,9 @@ def init_db():
|
|||||||
('ha_url',''),
|
('ha_url',''),
|
||||||
('ha_token',''),
|
('ha_token',''),
|
||||||
('ha_poll_interval','60'),
|
('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)
|
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 print_slots ADD COLUMN pieces_override INTEGER DEFAULT NULL',
|
||||||
'ALTER TABLE calendar_blocks ADD COLUMN date_end TEXT 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 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:
|
for sql in migrations:
|
||||||
try:
|
try:
|
||||||
@@ -456,7 +462,8 @@ def parse_3mf(file_stream):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct,
|
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.
|
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
|
wear = printer_w + nozzle_w + plate_w
|
||||||
elec = s['printer_power_kw'] * (print_time_s / 3600) * s['electricity_price_kwh'] / pieces_per_plate
|
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
|
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)
|
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
|
prix_ht = cout_fixe + total_marge
|
||||||
|
|
||||||
cot_rate = s.get('cotisations_rate_pct', 12.3) / 100
|
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),
|
'tva_amount': round(tva_amount, 4),
|
||||||
'prix_ht_net': round(prix_ht_net, 4),
|
'prix_ht_net': round(prix_ht_net, 4),
|
||||||
'prix_ttc': round(prix_ttc, 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),
|
'tax_amount': round(cot_amount + vfl_amount + other_taxes_amount, 4),
|
||||||
'final_price': round(final, 2),
|
'final_price': round(final, 2),
|
||||||
'_cot_pct': round(cot_rate * 100, 2),
|
'_cot_pct': round(cot_rate * 100, 2),
|
||||||
@@ -1311,19 +1330,24 @@ def new_job():
|
|||||||
if mat:
|
if mat:
|
||||||
price_kg = mat['price_per_kg']
|
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,
|
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,
|
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,
|
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,
|
weight_g,print_time_s,design_multiplier,gross_margin_pct,discount_pct,
|
||||||
pieces_per_plate,order_qty,
|
pieces_per_plate,order_qty,
|
||||||
material_cost,material_margin,design_cost,handling_cost,wear_cost,electricity_cost,
|
material_cost,material_margin,design_cost,handling_cost,wear_cost,electricity_cost,
|
||||||
cout_fixe,total_marge,subtotal,margin_amount,marge_pct_on_ht,
|
cout_fixe,total_marge,subtotal,margin_amount,marge_pct_on_ht,
|
||||||
cotisations_amount,vfl_amount,tva_amount,other_taxes_amount,tax_amount,
|
cotisations_amount,vfl_amount,tva_amount,other_taxes_amount,tax_amount,
|
||||||
price_per_piece,final_price,notes)
|
price_per_piece,final_price,notes)
|
||||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
|
||||||
(client_id,material_id,machine_pid,handling_pid,material_pid,pricing_pid,project_id,
|
(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',''),
|
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,
|
weight_g,print_time_s,design_mult,gross_margin,discount,
|
||||||
pieces_per_plate,order_qty,
|
pieces_per_plate,order_qty,
|
||||||
r['material_cost'],r['material_margin'],r['design_cost'],r['handling_cost'],
|
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)))
|
pieces_per_plate = max(1, int(d.get('pieces_per_plate', 1)))
|
||||||
order_qty = max(1, int(d.get('order_qty', 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
|
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(
|
result = calc(
|
||||||
weight_g=float(d['weight_g']),
|
weight_g=float(d['weight_g']),
|
||||||
@@ -1695,7 +1721,8 @@ def api_calculate():
|
|||||||
gross_margin_pct=float(d.get('gross_margin_pct',27.5)),
|
gross_margin_pct=float(d.get('gross_margin_pct',27.5)),
|
||||||
discount_pct=float(d.get('discount_pct',0)),
|
discount_pct=float(d.get('discount_pct',0)),
|
||||||
price_per_kg_override=price_kg, s=s,
|
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
|
# Multi-piece extras
|
||||||
nb_plateaux = math.ceil(order_qty / pieces_per_plate)
|
nb_plateaux = math.ceil(order_qty / pieces_per_plate)
|
||||||
|
|||||||
@@ -172,6 +172,9 @@
|
|||||||
<div class="breakdown-row section-header"><span>Partie variable</span></div>
|
<div class="breakdown-row section-header"><span>Partie variable</span></div>
|
||||||
{{ brow('Manutention', job.handling_cost, 'breakdown-muted ps-2', 4) }}
|
{{ brow('Manutention', job.handling_cost, 'breakdown-muted ps-2', 4) }}
|
||||||
{{ brow('Design (×' ~ job.design_multiplier ~ ')', job.design_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') }}
|
{{ brow('Marge brute (' ~ job.gross_margin_pct ~ '%)', job.margin_amount, 'breakdown-muted ps-2') }}
|
||||||
<div class="breakdown-row subtotal">
|
<div class="breakdown-row subtotal">
|
||||||
<span class="flex-fill fw-semibold">Total marge <small class="text-muted fw-normal">({{ marge_pct_ht }}% du HT)</small></span>
|
<span class="flex-fill fw-semibold">Total marge <small class="text-muted fw-normal">({{ marge_pct_ht }}% du HT)</small></span>
|
||||||
|
|||||||
+38
-2
@@ -285,6 +285,27 @@
|
|||||||
<label class="form-label fw-semibold">Notes internes</label>
|
<label class="form-label fw-semibold">Notes internes</label>
|
||||||
<textarea name="notes" class="form-control" rows="2">{{ prefill.notes if prefill else '' }}</textarea>
|
<textarea name="notes" class="form-control" rows="2">{{ prefill.notes if prefill else '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3 border rounded p-3" id="meshyBlock" style="background:#faf5ff">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="meshy_enabled" id="meshy_enabled"
|
||||||
|
value="1" onchange="onMeshyToggle()"
|
||||||
|
{% if prefill and prefill.meshy_enabled %}checked{% endif %}>
|
||||||
|
<label class="form-check-label fw-semibold" for="meshy_enabled" style="color:#7c3aed">
|
||||||
|
<i class="bi bi-stars me-1"></i>Génération Meshy.ai
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="meshyCreditsRow" class="{% if not (prefill and prefill.meshy_enabled) %}d-none{% endif %}">
|
||||||
|
<label class="form-label small fw-semibold text-muted mb-1">Crédits utilisés</label>
|
||||||
|
<div class="input-group input-group-sm" style="max-width:180px">
|
||||||
|
<input type="number" name="meshy_credits_used" id="meshy_credits_used"
|
||||||
|
class="form-control" min="0" step="1"
|
||||||
|
value="{{ prefill.meshy_credits_used if prefill else 0 }}"
|
||||||
|
oninput="recalc()">
|
||||||
|
<span class="input-group-text">crédits</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text" id="meshyCostHint"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -574,7 +595,7 @@ document.getElementById('materialSelect').addEventListener('change', function()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Autres champs → recalc ────────────────────────────────────────────────────
|
// ── 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);
|
const el = document.getElementById(id);
|
||||||
if (el) el.addEventListener('input', recalc);
|
if (el) el.addEventListener('input', recalc);
|
||||||
});
|
});
|
||||||
@@ -604,6 +625,8 @@ function recalc() {
|
|||||||
material_profile_id: document.getElementById('materialProfileSelect').value || null,
|
material_profile_id: document.getElementById('materialProfileSelect').value || null,
|
||||||
pieces_per_plate: parseInt(document.getElementById('pieces_per_plate').value) || 1,
|
pieces_per_plate: parseInt(document.getElementById('pieces_per_plate').value) || 1,
|
||||||
order_qty: parseInt(document.getElementById('order_qty').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', {
|
fetch('/api/calculate', {
|
||||||
@@ -612,7 +635,13 @@ function recalc() {
|
|||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => renderBreakdown(d, body))
|
.then(d => {
|
||||||
|
renderBreakdown(d, body);
|
||||||
|
if (d.meshy_unit_cost) {
|
||||||
|
document.getElementById('meshyCostHint').innerHTML =
|
||||||
|
`<span class="text-muted">Coût unitaire : ${d.meshy_unit_cost.toFixed(4)} €/crédit</span>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,6 +649,12 @@ function recalc() {
|
|||||||
function fmt(n) { return parseFloat(n).toFixed(4) + ' €'; }
|
function fmt(n) { return parseFloat(n).toFixed(4) + ' €'; }
|
||||||
function fmt2(n) { return parseFloat(n).toFixed(2) + ' €'; }
|
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) {
|
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);
|
||||||
@@ -729,6 +764,7 @@ function renderBreakdown(d, body) {
|
|||||||
<div class="breakdown-row section-header"><span>Partie variable</span></div>
|
<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">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)}
|
${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) : ''}
|
||||||
${row(`<span class="breakdown-muted ps-2">Marge brute (${mgn}%)</span>`, d.margin_amount)}
|
${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')}
|
${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')}
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Meshy.ai -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header py-3"><i class="bi bi-stars me-2" style="color:#7c3aed"></i>Meshy.ai — Génération 3D par IA</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-semibold">Coût abonnement annuel (€)</label>
|
||||||
|
<input type="number" name="meshy_annual_cost" class="form-control"
|
||||||
|
min="0" step="1"
|
||||||
|
value="{{ settings.get('meshy_annual_cost', 200)|int }}">
|
||||||
|
<div class="form-text">Montant total payé par an (ex: 200 € pour Meshy Pro).</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-semibold">Crédits mensuels inclus</label>
|
||||||
|
<input type="number" name="meshy_monthly_credits" class="form-control"
|
||||||
|
min="1" step="1"
|
||||||
|
value="{{ settings.get('meshy_monthly_credits', 1000)|int }}">
|
||||||
|
<div class="form-text">Nb de crédits inclus chaque mois dans l'abonnement.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-semibold">Multiplicateur de marge</label>
|
||||||
|
<input type="number" name="meshy_cost_multiplier" class="form-control"
|
||||||
|
min="1" step="0.1"
|
||||||
|
value="{{ settings.get('meshy_cost_multiplier', 1.0) }}">
|
||||||
|
<div class="form-text">
|
||||||
|
×1.0 = répercute le coût exact. ×1.5 = coût + 50% de marge sur Meshy.
|
||||||
|
<br>Coût unitaire actuel :
|
||||||
|
<strong>
|
||||||
|
{% set unit = settings.get('meshy_annual_cost', 200) / 12 / [settings.get('meshy_monthly_credits', 1000), 1]|max %}
|
||||||
|
{{ "%.4f"|format(unit) }} €/crédit
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- iCal -->
|
<!-- iCal -->
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Reference in New Issue
Block a user