feat: profils par défaut par client + fix marge 0% + fix: suggest-slot ignore les créneaux déjà passés
Deploy via Portainer / deploy (push) Successful in 0s
Deploy via Portainer / deploy (push) Successful in 0s
This commit is contained in:
@@ -173,6 +173,10 @@ def init_db():
|
|||||||
'ALTER TABLE jobs ADD COLUMN pieces_per_plate INTEGER DEFAULT 1',
|
'ALTER TABLE jobs ADD COLUMN pieces_per_plate INTEGER DEFAULT 1',
|
||||||
'ALTER TABLE jobs ADD COLUMN order_qty INTEGER DEFAULT 1',
|
'ALTER TABLE jobs ADD COLUMN order_qty INTEGER DEFAULT 1',
|
||||||
'ALTER TABLE jobs ADD COLUMN price_per_piece REAL',
|
'ALTER TABLE jobs ADD COLUMN price_per_piece REAL',
|
||||||
|
'ALTER TABLE clients ADD COLUMN default_machine_profile_id INTEGER',
|
||||||
|
'ALTER TABLE clients ADD COLUMN default_handling_profile_id INTEGER',
|
||||||
|
'ALTER TABLE clients ADD COLUMN default_material_profile_id INTEGER',
|
||||||
|
'ALTER TABLE clients ADD COLUMN default_pricing_profile_id INTEGER',
|
||||||
]
|
]
|
||||||
for sql in migrations:
|
for sql in migrations:
|
||||||
try:
|
try:
|
||||||
@@ -453,7 +457,10 @@ def find_next_slot(print_time_s):
|
|||||||
candidates.append(datetime(d.year, d.month, d.day, h0, m0))
|
candidates.append(datetime(d.year, d.month, d.day, h0, m0))
|
||||||
candidates.append(datetime(d.year, d.month, d.day, h1, m1))
|
candidates.append(datetime(d.year, d.month, d.day, h1, m1))
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
for start_dt in candidates:
|
for start_dt in candidates:
|
||||||
|
if start_dt <= now: # ignorer les créneaux déjà passés
|
||||||
|
continue
|
||||||
end_dt = start_dt + timedelta(seconds=total_sec)
|
end_dt = start_dt + timedelta(seconds=total_sec)
|
||||||
for printer in printers:
|
for printer in printers:
|
||||||
pid = printer['id']
|
pid = printer['id']
|
||||||
@@ -516,30 +523,64 @@ def clients():
|
|||||||
conn.close()
|
conn.close()
|
||||||
return render_template('clients.html', clients=clients)
|
return render_template('clients.html', clients=clients)
|
||||||
|
|
||||||
|
def _get_all_profiles(conn):
|
||||||
|
return {
|
||||||
|
'machine_profiles': conn.execute('SELECT id,name FROM machine_profiles ORDER BY name').fetchall(),
|
||||||
|
'handling_profiles': conn.execute('SELECT id,name FROM handling_profiles ORDER BY name').fetchall(),
|
||||||
|
'material_profiles': conn.execute('SELECT id,name FROM material_profiles ORDER BY name').fetchall(),
|
||||||
|
'pricing_profiles': conn.execute('SELECT id,name FROM pricing_profiles ORDER BY name').fetchall(),
|
||||||
|
}
|
||||||
|
|
||||||
@app.route('/clients/new', methods=['GET','POST'])
|
@app.route('/clients/new', methods=['GET','POST'])
|
||||||
def new_client():
|
def new_client():
|
||||||
|
conn = get_db()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
conn = get_db()
|
def _int(k): return int(request.form[k]) if request.form.get(k) else None
|
||||||
conn.execute('INSERT INTO clients(name,email,design_multiplier,notes) VALUES(?,?,?,?)',
|
conn.execute('''INSERT INTO clients
|
||||||
(request.form['name'], request.form.get('email',''),
|
(name,email,notes,default_machine_profile_id,default_handling_profile_id,
|
||||||
float(request.form.get('design_multiplier', 1.80)), request.form.get('notes','')))
|
default_material_profile_id,default_pricing_profile_id)
|
||||||
|
VALUES(?,?,?,?,?,?,?)''',
|
||||||
|
(request.form['name'], request.form.get('email',''), request.form.get('notes',''),
|
||||||
|
_int('default_machine_profile_id'), _int('default_handling_profile_id'),
|
||||||
|
_int('default_material_profile_id'), _int('default_pricing_profile_id')))
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return redirect(url_for('clients'))
|
return redirect(url_for('clients'))
|
||||||
return render_template('client_form.html', client=None)
|
profiles = _get_all_profiles(conn)
|
||||||
|
conn.close()
|
||||||
|
return render_template('client_form.html', client=None, **profiles)
|
||||||
|
|
||||||
@app.route('/clients/<int:id>/edit', methods=['GET','POST'])
|
@app.route('/clients/<int:id>/edit', methods=['GET','POST'])
|
||||||
def edit_client(id):
|
def edit_client(id):
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
client = conn.execute('SELECT * FROM clients WHERE id=?',(id,)).fetchone()
|
client = conn.execute('SELECT * FROM clients WHERE id=?',(id,)).fetchone()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
conn.execute('UPDATE clients SET name=?,email=?,design_multiplier=?,notes=? WHERE id=?',
|
def _int(k): return int(request.form[k]) if request.form.get(k) else None
|
||||||
(request.form['name'], request.form.get('email',''),
|
conn.execute('''UPDATE clients SET name=?,email=?,notes=?,
|
||||||
float(request.form.get('design_multiplier', client['design_multiplier'])),
|
default_machine_profile_id=?,default_handling_profile_id=?,
|
||||||
request.form.get('notes',''), id))
|
default_material_profile_id=?,default_pricing_profile_id=? WHERE id=?''',
|
||||||
|
(request.form['name'], request.form.get('email',''), request.form.get('notes',''),
|
||||||
|
_int('default_machine_profile_id'), _int('default_handling_profile_id'),
|
||||||
|
_int('default_material_profile_id'), _int('default_pricing_profile_id'), id))
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return redirect(url_for('clients'))
|
return redirect(url_for('clients'))
|
||||||
|
profiles = _get_all_profiles(conn)
|
||||||
conn.close()
|
conn.close()
|
||||||
return render_template('client_form.html', client=client)
|
return render_template('client_form.html', client=client, **profiles)
|
||||||
|
|
||||||
|
@app.route('/api/client/<int:id>/defaults')
|
||||||
|
def api_client_defaults(id):
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.execute('SELECT default_machine_profile_id, default_handling_profile_id, \
|
||||||
|
default_material_profile_id, default_pricing_profile_id FROM clients WHERE id=?',(id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not c:
|
||||||
|
return jsonify({})
|
||||||
|
return jsonify({
|
||||||
|
'machine_profile_id': c['default_machine_profile_id'],
|
||||||
|
'handling_profile_id': c['default_handling_profile_id'],
|
||||||
|
'material_profile_id': c['default_material_profile_id'],
|
||||||
|
'pricing_profile_id': c['default_pricing_profile_id'],
|
||||||
|
})
|
||||||
|
|
||||||
@app.route('/clients/<int:id>/delete', methods=['POST'])
|
@app.route('/clients/<int:id>/delete', methods=['POST'])
|
||||||
def delete_client(id):
|
def delete_client(id):
|
||||||
|
|||||||
@@ -10,10 +10,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-7">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Nom *</label>
|
<label class="form-label fw-semibold">Nom *</label>
|
||||||
<input type="text" name="name" class="form-control" required
|
<input type="text" name="name" class="form-control" required
|
||||||
@@ -26,9 +27,67 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label fw-semibold">Notes</label>
|
<label class="form-label fw-semibold">Notes</label>
|
||||||
<textarea name="notes" class="form-control" rows="3">{{ client.notes if client else '' }}</textarea>
|
<textarea name="notes" class="form-control" rows="2">{{ client.notes if client else '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
|
||||||
|
<hr>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="fw-semibold"><i class="bi bi-bookmark-star me-1 text-secondary"></i>Profils par défaut</span>
|
||||||
|
<div class="form-text mb-3">Pré-sélectionnés automatiquement dans "Nouveau calcul" quand ce client est choisi.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Profil machine</label>
|
||||||
|
<select name="default_machine_profile_id" class="form-select">
|
||||||
|
<option value="">— aucun —</option>
|
||||||
|
{% for p in machine_profiles %}
|
||||||
|
<option value="{{ p.id }}"
|
||||||
|
{% if client and client.default_machine_profile_id == p.id %}selected{% endif %}>
|
||||||
|
{{ p.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Profil manutention</label>
|
||||||
|
<select name="default_handling_profile_id" class="form-select">
|
||||||
|
<option value="">— aucun —</option>
|
||||||
|
{% for p in handling_profiles %}
|
||||||
|
<option value="{{ p.id }}"
|
||||||
|
{% if client and client.default_handling_profile_id == p.id %}selected{% endif %}>
|
||||||
|
{{ p.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Profil matière</label>
|
||||||
|
<select name="default_material_profile_id" class="form-select">
|
||||||
|
<option value="">— aucun —</option>
|
||||||
|
{% for p in material_profiles %}
|
||||||
|
<option value="{{ p.id }}"
|
||||||
|
{% if client and client.default_material_profile_id == p.id %}selected{% endif %}>
|
||||||
|
{{ p.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Profil tarif client</label>
|
||||||
|
<select name="default_pricing_profile_id" class="form-select">
|
||||||
|
<option value="">— aucun —</option>
|
||||||
|
{% for p in pricing_profiles %}
|
||||||
|
<option value="{{ p.id }}"
|
||||||
|
{% if client and client.default_pricing_profile_id == p.id %}selected{% endif %}>
|
||||||
|
{{ p.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mt-4">
|
||||||
<button type="submit" class="btn" style="background:#ff6b35;color:#fff">
|
<button type="submit" class="btn" style="background:#ff6b35;color:#fff">
|
||||||
<i class="bi bi-save me-1"></i>Enregistrer
|
<i class="bi bi-save me-1"></i>Enregistrer
|
||||||
</button>
|
</button>
|
||||||
@@ -40,5 +99,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
|
||||||
|
|||||||
+24
-8
@@ -328,14 +328,30 @@ document.getElementById('pricingProfileSelect').addEventListener('change', funct
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Client : auto-remplir DM ──────────────────────────────────────────────────
|
// ── Client : auto-remplir profils par défaut ─────────────────────────────────
|
||||||
document.getElementById('clientSelect').addEventListener('change', function() {
|
document.getElementById('clientSelect').addEventListener('change', function() {
|
||||||
const dm = this.options[this.selectedIndex].dataset.dm;
|
const clientId = this.value;
|
||||||
if (dm) {
|
if (!clientId) return;
|
||||||
document.getElementById('design_multiplier').value = parseFloat(dm);
|
fetch('/api/client/' + clientId + '/defaults')
|
||||||
updateDMLabel(parseFloat(dm));
|
.then(r => r.json())
|
||||||
recalc();
|
.then(d => {
|
||||||
}
|
if (d.machine_profile_id != null) document.getElementById('machineProfileSelect').value = d.machine_profile_id;
|
||||||
|
if (d.handling_profile_id != null) document.getElementById('handlingProfileSelect').value = d.handling_profile_id;
|
||||||
|
if (d.material_profile_id != null) document.getElementById('materialProfileSelect').value = d.material_profile_id;
|
||||||
|
if (d.pricing_profile_id != null) {
|
||||||
|
const sel = document.getElementById('pricingProfileSelect');
|
||||||
|
sel.value = d.pricing_profile_id;
|
||||||
|
// Appliquer DM + marge du profil tarif sélectionné
|
||||||
|
const opt = sel.options[sel.selectedIndex];
|
||||||
|
if (opt && opt.value) {
|
||||||
|
const dm = parseFloat(opt.dataset.dm);
|
||||||
|
const margin = parseFloat(opt.dataset.margin);
|
||||||
|
if (!isNaN(dm)) { document.getElementById('design_multiplier').value = dm; updateDMLabel(dm); }
|
||||||
|
if (!isNaN(margin)) { marginSlider.value = margin; updateMarginDisplay(margin); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recalc();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Matiere : afficher stock ──────────────────────────────────────────────────
|
// ── Matiere : afficher stock ──────────────────────────────────────────────────
|
||||||
@@ -374,7 +390,7 @@ function recalc() {
|
|||||||
hours: h,
|
hours: h,
|
||||||
minutes: m,
|
minutes: m,
|
||||||
design_multiplier: parseFloat(document.getElementById('design_multiplier').value) || 1.80,
|
design_multiplier: parseFloat(document.getElementById('design_multiplier').value) || 1.80,
|
||||||
gross_margin_pct: parseFloat(document.getElementById('gross_margin_pct').value) || 27.5,
|
gross_margin_pct: parseFloat(document.getElementById('gross_margin_pct').value ?? 27.5),
|
||||||
discount_pct: parseFloat(document.getElementById('discount_pct').value) || 0,
|
discount_pct: parseFloat(document.getElementById('discount_pct').value) || 0,
|
||||||
material_id: document.getElementById('materialSelect').value || null,
|
material_id: document.getElementById('materialSelect').value || null,
|
||||||
machine_profile_id: document.getElementById('machineProfileSelect').value || null,
|
machine_profile_id: document.getElementById('machineProfileSelect').value || null,
|
||||||
|
|||||||
Reference in New Issue
Block a user