feat: plate_name + pieces auto-fill + object names from 3MF
Deploy via Portainer / deploy (push) Successful in 0s
Deploy via Portainer / deploy (push) Successful in 0s
This commit is contained in:
@@ -151,7 +151,7 @@ def init_db():
|
|||||||
handling_profile_id INTEGER REFERENCES handling_profiles(id) ON DELETE SET NULL,
|
handling_profile_id INTEGER REFERENCES handling_profiles(id) ON DELETE SET NULL,
|
||||||
material_profile_id INTEGER REFERENCES material_profiles(id) ON DELETE SET NULL,
|
material_profile_id INTEGER REFERENCES material_profiles(id) ON DELETE SET NULL,
|
||||||
pricing_profile_id INTEGER REFERENCES pricing_profiles(id) ON DELETE SET NULL,
|
pricing_profile_id INTEGER REFERENCES pricing_profiles(id) ON DELETE SET NULL,
|
||||||
source_file TEXT DEFAULT '', name TEXT NOT NULL,
|
source_file TEXT DEFAULT '', plate_name TEXT DEFAULT '', name TEXT NOT NULL,
|
||||||
description TEXT DEFAULT '', weight_g REAL NOT NULL,
|
description TEXT DEFAULT '', weight_g REAL NOT NULL,
|
||||||
print_time_s INTEGER NOT NULL, design_multiplier REAL NOT NULL,
|
print_time_s INTEGER NOT NULL, design_multiplier REAL NOT NULL,
|
||||||
gross_margin_pct REAL NOT NULL DEFAULT 27.5, discount_pct REAL DEFAULT 0,
|
gross_margin_pct REAL NOT NULL DEFAULT 27.5, discount_pct REAL DEFAULT 0,
|
||||||
@@ -259,6 +259,7 @@ def init_db():
|
|||||||
'ALTER TABLE print_slots ADD COLUMN wasted_g REAL DEFAULT 0',
|
'ALTER TABLE print_slots ADD COLUMN wasted_g REAL DEFAULT 0',
|
||||||
'ALTER TABLE materials ADD COLUMN total_wasted_g REAL DEFAULT 0',
|
'ALTER TABLE materials ADD COLUMN total_wasted_g REAL DEFAULT 0',
|
||||||
'ALTER TABLE jobs ADD COLUMN client_token TEXT DEFAULT NULL',
|
'ALTER TABLE jobs ADD COLUMN client_token TEXT DEFAULT NULL',
|
||||||
|
'ALTER TABLE jobs ADD COLUMN plate_name TEXT DEFAULT ""',
|
||||||
]
|
]
|
||||||
for sql in migrations:
|
for sql in migrations:
|
||||||
try:
|
try:
|
||||||
@@ -304,6 +305,7 @@ def parse_3mf(file_stream):
|
|||||||
meta = {m.get('key'): m.get('value') for m in plate_el.findall('metadata')}
|
meta = {m.get('key'): m.get('value') for m in plate_el.findall('metadata')}
|
||||||
plate['index'] = int(meta.get('index', 0))
|
plate['index'] = int(meta.get('index', 0))
|
||||||
plate['print_time_s'] = int(meta.get('prediction', 0))
|
plate['print_time_s'] = int(meta.get('prediction', 0))
|
||||||
|
plate['name'] = meta.get('name', '') or ''
|
||||||
|
|
||||||
filaments = []
|
filaments = []
|
||||||
total_g = 0.0
|
total_g = 0.0
|
||||||
@@ -317,12 +319,26 @@ def parse_3mf(file_stream):
|
|||||||
'used_g': round(used_g, 2),
|
'used_g': round(used_g, 2),
|
||||||
'used_m': round(float(fil.get('used_m', 0) or 0), 2),
|
'used_m': round(float(fil.get('used_m', 0) or 0), 2),
|
||||||
})
|
})
|
||||||
plate['weight_g'] = round(total_g, 2)
|
plate['weight_g'] = round(total_g, 2)
|
||||||
plate['filaments'] = filaments
|
plate['filaments'] = filaments
|
||||||
|
# Pièces : compter les <object> non skippés
|
||||||
|
objs = [o for o in plate_el.findall('object') if o.get('skipped') != 'true']
|
||||||
|
plate['pieces'] = len(objs)
|
||||||
|
# Noms uniques des modèles (sans extension)
|
||||||
|
plate['object_names'] = list(dict.fromkeys(
|
||||||
|
o.get('name', '').rsplit('.', 1)[0]
|
||||||
|
for o in objs if o.get('name')
|
||||||
|
))
|
||||||
# Libellé lisible
|
# Libellé lisible
|
||||||
h = plate['print_time_s'] // 3600
|
h = plate['print_time_s'] // 3600
|
||||||
mn = (plate['print_time_s'] % 3600) // 60
|
mn = (plate['print_time_s'] % 3600) // 60
|
||||||
plate['label'] = f"Plateau {plate['index']} — {plate['weight_g']} g — {h}h{mn:02d}m"
|
name_part = f" «{plate['name']}»" if plate['name'] else ''
|
||||||
|
plate['label'] = (
|
||||||
|
f"Plateau {plate['index']}{name_part}"
|
||||||
|
f" — {plate['weight_g']} g"
|
||||||
|
f" — {h}h{mn:02d}m"
|
||||||
|
f" — {plate['pieces']} pce(s)"
|
||||||
|
)
|
||||||
result['plates'].append(plate)
|
result['plates'].append(plate)
|
||||||
result['plates'].sort(key=lambda p: p['index'])
|
result['plates'].sort(key=lambda p: p['index'])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1157,6 +1173,7 @@ def new_job():
|
|||||||
project_id = request.form.get('project_id') or None
|
project_id = request.form.get('project_id') or None
|
||||||
pieces_per_plate = max(1, int(request.form.get('pieces_per_plate', 1)))
|
pieces_per_plate = max(1, int(request.form.get('pieces_per_plate', 1)))
|
||||||
order_qty = max(1, int(request.form.get('order_qty', 1)))
|
order_qty = max(1, int(request.form.get('order_qty', 1)))
|
||||||
|
plate_name = request.form.get('plate_name', '').strip()
|
||||||
|
|
||||||
s = dict(settings)
|
s = dict(settings)
|
||||||
if machine_pid:
|
if machine_pid:
|
||||||
@@ -1189,16 +1206,16 @@ def new_job():
|
|||||||
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)
|
||||||
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,name,description,
|
material_profile_id,pricing_profile_id,project_id,source_file,plate_name,name,description,
|
||||||
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,
|
cout_fixe,total_marge,subtotal,margin_amount,
|
||||||
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,request.form['name'],request.form.get('description',''),
|
source_file,plate_name,request.form['name'],request.form.get('description',''),
|
||||||
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'],
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}—{% endif %}
|
{% else %}—{% endif %}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
|
{% if job.plate_name %}
|
||||||
|
<tr><th class="text-muted fw-normal">Référence plateau</th>
|
||||||
|
<td><span class="badge bg-light text-dark border">{{ job.plate_name }}</span></td></tr>
|
||||||
|
{% endif %}
|
||||||
<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 plateau</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">Pieces / plateau</th><td>{{ job.pieces_per_plate or 1 }}</td></tr>
|
<tr><th class="text-muted fw-normal">Pieces / plateau</th><td>{{ job.pieces_per_plate or 1 }}</td></tr>
|
||||||
|
|||||||
+38
-7
@@ -152,6 +152,15 @@
|
|||||||
<div class="form-text">Nombre total de pièces à produire.</div>
|
<div class="form-text">Nombre total de pièces à produire.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row g-3 mt-0">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-semibold">Nom / référence du plateau <span class="text-muted fw-normal small">(optionnel)</span></label>
|
||||||
|
<input type="text" name="plate_name" id="plate_name" class="form-control"
|
||||||
|
placeholder="ex : «Plateau 2» ou nom custom Bambu Studio"
|
||||||
|
value="{{ prefill.plate_name if prefill else '' }}">
|
||||||
|
<div id="plateObjectsHint" class="form-text"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -387,7 +396,11 @@ function parse3mf() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ── Plaque unique ou fallback ──
|
// ── Plaque unique ou fallback ──
|
||||||
applyPlateData(d.weight_g, d.hours, d.minutes);
|
const p0 = (d.plates && d.plates.length === 1) ? d.plates[0] : null;
|
||||||
|
applyPlateData(d.weight_g, d.hours, d.minutes,
|
||||||
|
p0 ? p0.pieces : null,
|
||||||
|
p0 ? (p0.name || `Plateau ${p0.index}`) : null,
|
||||||
|
p0 ? p0.object_names : null);
|
||||||
const ok = d.weight_g || d.hours != null;
|
const ok = d.weight_g || d.hours != null;
|
||||||
status.innerHTML = ok
|
status.innerHTML = ok
|
||||||
? '<span class="text-success"><i class="bi bi-check-circle me-1"></i>Données extraites</span>'
|
? '<span class="text-success"><i class="bi bi-check-circle me-1"></i>Données extraites</span>'
|
||||||
@@ -396,10 +409,19 @@ function parse3mf() {
|
|||||||
.catch(() => { status.innerHTML = '<span class="text-danger">Erreur de lecture</span>'; });
|
.catch(() => { status.innerHTML = '<span class="text-danger">Erreur de lecture</span>'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPlateData(weight_g, hours, minutes) {
|
function applyPlateData(weight_g, hours, minutes, pieces, plate_name, object_names) {
|
||||||
if (weight_g != null) document.getElementById('weight_g').value = weight_g;
|
if (weight_g != null) document.getElementById('weight_g').value = weight_g;
|
||||||
if (hours != null) document.getElementById('hours').value = hours;
|
if (hours != null) document.getElementById('hours').value = hours;
|
||||||
if (minutes != null) document.getElementById('minutes').value = minutes;
|
if (minutes != null) document.getElementById('minutes').value = minutes;
|
||||||
|
if (pieces != null) document.getElementById('pieces_per_plate').value = pieces;
|
||||||
|
if (plate_name != null) document.getElementById('plate_name').value = plate_name;
|
||||||
|
// Afficher les noms des modèles comme aide
|
||||||
|
const hint = document.getElementById('plateObjectsHint');
|
||||||
|
if (hint && object_names && object_names.length) {
|
||||||
|
hint.innerHTML = `<i class="bi bi-layers me-1 text-muted"></i>Modèles : <span class="text-muted">${object_names.join(', ')}</span>`;
|
||||||
|
} else if (hint) {
|
||||||
|
hint.textContent = '';
|
||||||
|
}
|
||||||
recalc();
|
recalc();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,11 +442,20 @@ function renderPlateSelector(plates) {
|
|||||||
const dots = p.filaments.map(f =>
|
const dots = p.filaments.map(f =>
|
||||||
`<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${f.color || '#ccc'};border:1px solid #aaa;margin-right:2px"></span>`
|
`<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${f.color || '#ccc'};border:1px solid #aaa;margin-right:2px"></span>`
|
||||||
).join('');
|
).join('');
|
||||||
btn.innerHTML = `${dots}<strong>Plateau ${p.index}</strong> — ${p.weight_g} g — ${Math.floor(p.print_time_s/3600)}h${String(Math.floor((p.print_time_s%3600)/60)).padStart(2,'0')}m`;
|
const piecesLabel = p.pieces != null ? ` — ${p.pieces} pce` : '';
|
||||||
|
const nameLabel = p.name ? ` «${p.name}»` : '';
|
||||||
|
btn.innerHTML = `${dots}<strong>Plateau ${p.index}${nameLabel}</strong> — ${p.weight_g} g — ${Math.floor(p.print_time_s/3600)}h${String(Math.floor((p.print_time_s%3600)/60)).padStart(2,'0')}m${piecesLabel}`;
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
document.querySelectorAll('#plateSelectorWrap button').forEach(b => b.classList.replace('btn-warning','btn-outline-warning'));
|
document.querySelectorAll('#plateSelectorWrap button').forEach(b => b.classList.replace('btn-warning','btn-outline-warning'));
|
||||||
btn.classList.replace('btn-outline-warning','btn-warning');
|
btn.classList.replace('btn-outline-warning','btn-warning');
|
||||||
applyPlateData(p.weight_g, Math.floor(p.print_time_s/3600), Math.floor((p.print_time_s%3600)/60));
|
applyPlateData(
|
||||||
|
p.weight_g,
|
||||||
|
Math.floor(p.print_time_s/3600),
|
||||||
|
Math.floor((p.print_time_s%3600)/60),
|
||||||
|
p.pieces,
|
||||||
|
p.name || `Plateau ${p.index}`,
|
||||||
|
p.object_names
|
||||||
|
);
|
||||||
document.getElementById('parseStatus').innerHTML =
|
document.getElementById('parseStatus').innerHTML =
|
||||||
`<span class="text-success"><i class="bi bi-check-circle me-1"></i>Plateau ${p.index} appliqué</span>`;
|
`<span class="text-success"><i class="bi bi-check-circle me-1"></i>Plateau ${p.index} appliqué</span>`;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user