feat: max simultaneous printers enforced + capacity bands on Gantt
Deploy via Portainer / deploy (push) Successful in 0s
Deploy via Portainer / deploy (push) Successful in 0s
This commit is contained in:
@@ -1304,8 +1304,10 @@ def planning():
|
||||
).fetchall()
|
||||
schedule = conn.execute('SELECT * FROM working_schedule ORDER BY day_of_week').fetchall()
|
||||
conn.close()
|
||||
settings = get_settings()
|
||||
return render_template('planning.html', printers=printers,
|
||||
unscheduled=unscheduled, blocks=blocks, schedule=schedule)
|
||||
unscheduled=unscheduled, blocks=blocks, schedule=schedule,
|
||||
settings=settings)
|
||||
|
||||
@app.route('/printers', methods=['GET','POST'])
|
||||
def printers_page():
|
||||
@@ -1620,17 +1622,71 @@ def api_slots():
|
||||
ps.status, ps.failure_note,
|
||||
j.name as job_name, j.print_time_s, j.final_price,
|
||||
j.pieces_per_plate, j.order_qty, j.weight_g,
|
||||
j.plate_name,
|
||||
p.name as printer_name,
|
||||
c.name as client_name
|
||||
c.name as client_name,
|
||||
m.name as material_name, m.type as material_type, m.color as material_color
|
||||
FROM print_slots ps
|
||||
JOIN jobs j ON ps.job_id=j.id
|
||||
JOIN printers p ON ps.printer_id=p.id
|
||||
LEFT JOIN clients c ON j.client_id=c.id
|
||||
LEFT JOIN materials m ON j.material_id=m.id
|
||||
WHERE ps.planned_start >= datetime('now', '-7 days')
|
||||
ORDER BY ps.planned_start''').fetchall()
|
||||
conn.close()
|
||||
return jsonify([dict(s) for s in slots])
|
||||
|
||||
def _check_capacity(conn, start_dt, end_dt, max_printers, exclude_id=None):
|
||||
"""Vérifie si ajouter ce créneau dépasserait max_printers simultanés.
|
||||
Retourne (True, peak_count) si limite atteinte, (False, peak_count) sinon.
|
||||
Méthode : on évalue le nombre de slots actifs à chaque 'event point'
|
||||
(toutes les heures de début des créneaux qui chevauchent la fenêtre).
|
||||
"""
|
||||
q = """SELECT planned_start, planned_end FROM print_slots
|
||||
WHERE status NOT IN ('cancelled','failed','done')
|
||||
AND planned_start < ? AND planned_end > ?"""
|
||||
params = [end_dt.isoformat(), start_dt.isoformat()]
|
||||
if exclude_id:
|
||||
q += ' AND id != ?'
|
||||
params.append(exclude_id)
|
||||
overlapping = conn.execute(q, params).fetchall()
|
||||
|
||||
if len(overlapping) < max_printers:
|
||||
return False, len(overlapping) # pas assez de chevauchements pour atteindre la limite
|
||||
|
||||
# Points à tester : le début du créneau proposé + début de chaque slot existant dans la fenêtre
|
||||
event_times = [start_dt]
|
||||
for sl in overlapping:
|
||||
t = datetime.fromisoformat(sl['planned_start'])
|
||||
if start_dt <= t < end_dt:
|
||||
event_times.append(t)
|
||||
|
||||
peak = 0
|
||||
for t in event_times:
|
||||
count = sum(
|
||||
1 for sl in overlapping
|
||||
if datetime.fromisoformat(sl['planned_start']) <= t
|
||||
< datetime.fromisoformat(sl['planned_end'])
|
||||
)
|
||||
if count > peak:
|
||||
peak = count
|
||||
if count >= max_printers:
|
||||
return True, count
|
||||
return False, peak
|
||||
|
||||
def _check_overlap(conn, printer_id, start_dt, end_dt, exclude_id=None):
|
||||
"""Retourne le slot en conflit s'il existe, sinon None."""
|
||||
q = """SELECT ps.id, j.name as job_name, ps.planned_start, ps.planned_end
|
||||
FROM print_slots ps JOIN jobs j ON ps.job_id=j.id
|
||||
WHERE ps.printer_id=?
|
||||
AND ps.status NOT IN ('cancelled','failed','done')
|
||||
AND ps.planned_start < ? AND ps.planned_end > ?"""
|
||||
params = [printer_id, end_dt.isoformat(), start_dt.isoformat()]
|
||||
if exclude_id:
|
||||
q += ' AND ps.id != ?'
|
||||
params.append(exclude_id)
|
||||
return conn.execute(q, params).fetchone()
|
||||
|
||||
@app.route('/api/slots/new', methods=['POST'])
|
||||
def api_new_slot():
|
||||
d = request.json
|
||||
@@ -1646,6 +1702,16 @@ def api_new_slot():
|
||||
cooldown_s = int(s.get('cooldown_minutes', 15)) * 60
|
||||
start_dt = datetime.fromisoformat(start_str)
|
||||
end_dt = start_dt + timedelta(seconds=job['print_time_s'] + cooldown_s)
|
||||
conflict = _check_overlap(conn, printer_id, start_dt, end_dt)
|
||||
if conflict:
|
||||
conn.close()
|
||||
end_conf = conflict['planned_end'][:16].replace('T', ' ')
|
||||
return jsonify({'error': f"Chevauchement avec « {conflict['job_name']} » (fin prévue {end_conf}). Choisissez un autre horaire ou une autre imprimante."}), 409
|
||||
max_printers = int(s.get('max_simultaneous_printers', 5))
|
||||
over, peak = _check_capacity(conn, start_dt, end_dt, max_printers)
|
||||
if over:
|
||||
conn.close()
|
||||
return jsonify({'error': f"Capacité maximale atteinte : {peak} impression(s) déjà en cours sur ce créneau (limite : {max_printers} simultanées). Décalez l'horaire ou attendez qu'une imprimante se libère."}), 409
|
||||
conn.execute('INSERT INTO print_slots(job_id,printer_id,planned_start,planned_end,status) VALUES(?,?,?,?,?)',
|
||||
(job_id, printer_id, start_dt.isoformat(), end_dt.isoformat(), 'planned'))
|
||||
conn.commit(); conn.close()
|
||||
@@ -1664,6 +1730,16 @@ def api_move_slot(id):
|
||||
cooldown_s = int(s.get('cooldown_minutes', 15)) * 60
|
||||
start_dt = datetime.fromisoformat(start_str)
|
||||
end_dt = start_dt + timedelta(seconds=slot['print_time_s'] + cooldown_s)
|
||||
conflict = _check_overlap(conn, slot['printer_id'], start_dt, end_dt, exclude_id=id)
|
||||
if conflict:
|
||||
conn.close()
|
||||
end_conf = conflict['planned_end'][:16].replace('T', ' ')
|
||||
return jsonify({'error': f"Chevauchement avec « {conflict['job_name']} » (fin prévue {end_conf})."}), 409
|
||||
max_printers = int(s.get('max_simultaneous_printers', 5))
|
||||
over, peak = _check_capacity(conn, start_dt, end_dt, max_printers, exclude_id=id)
|
||||
if over:
|
||||
conn.close()
|
||||
return jsonify({'error': f"Capacité maximale atteinte : {peak} impression(s) en cours sur ce créneau (limite : {max_printers} simultanées)."}), 409
|
||||
conn.execute('UPDATE print_slots SET planned_start=?,planned_end=? WHERE id=?',
|
||||
(start_dt.isoformat(), end_dt.isoformat(), id))
|
||||
conn.commit(); conn.close()
|
||||
|
||||
Reference in New Issue
Block a user