diff --git a/app.py b/app.py index 37942da..84317a1 100644 --- a/app.py +++ b/app.py @@ -269,6 +269,7 @@ def init_db(): 'ALTER TABLE printers ADD COLUMN ha_entity_prefix TEXT DEFAULT ""', 'ALTER TABLE print_slots ADD COLUMN pieces_ignored INTEGER DEFAULT 0', 'ALTER TABLE print_slots ADD COLUMN pieces_override INTEGER DEFAULT NULL', + 'ALTER TABLE calendar_blocks ADD COLUMN date_end TEXT DEFAULT NULL', ] for sql in migrations: try: @@ -516,9 +517,14 @@ def get_schedule_dict(): def get_blocks_dict(from_date=None, to_date=None): conn = get_db() if from_date and to_date: + # A block overlaps [from_date, to_date] when: + # block.date <= to_date AND COALESCE(block.date_end, block.date) >= from_date rows = conn.execute( - 'SELECT * FROM calendar_blocks WHERE date BETWEEN ? AND ? ORDER BY date', - (str(from_date), str(to_date))).fetchall() + """SELECT * FROM calendar_blocks + WHERE date <= ? + AND COALESCE(date_end, date) >= ? + ORDER BY date""", + (str(to_date), str(from_date))).fetchall() else: rows = conn.execute('SELECT * FROM calendar_blocks ORDER BY date').fetchall() conn.close() @@ -528,7 +534,10 @@ def get_day_launches(check_date, schedule, blocks, cycle_min): """Returns number of possible launches on a given date (per machine).""" date_str = str(check_date) dow = check_date.weekday() - block = next((b for b in blocks if b['date'] == date_str and b['printer_id'] is None), None) + block = next((b for b in blocks + if b['date'] <= date_str + and (b['date_end'] or b['date']) >= date_str + and b['printer_id'] is None), None) mode = block['type'] if block else schedule[dow]['mode'] if mode == 'off': @@ -1282,13 +1291,20 @@ def job_detail(id): LEFT JOIN pricing_profiles pp ON j.pricing_profile_id=pp.id WHERE j.id=?''', (id,)).fetchone() slots = conn.execute(''' - SELECT ps.*, p.name as printer_name FROM print_slots ps + SELECT ps.*, p.name as printer_name, p.ha_entity_prefix, + COALESCE(ps.pieces_override, j.pieces_per_plate) as effective_pieces, + COALESCE(ps.pieces_ignored, 0) as pieces_ignored + FROM print_slots ps LEFT JOIN printers p ON ps.printer_id=p.id + JOIN jobs j ON ps.job_id=j.id WHERE ps.job_id=? ORDER BY ps.planned_start''', (id,)).fetchall() conn.close() if not job: return redirect(url_for('jobs')) - return render_template('job_detail.html', job=job, slots=slots) + import math as _math + total_plates_needed = _math.ceil((job['order_qty'] or 1) / max(1, job['pieces_per_plate'] or 1)) + return render_template('job_detail.html', job=job, slots=slots, + total_plates_needed=total_plates_needed) @app.route('/jobs//delete', methods=['POST']) def delete_job(id): @@ -1309,7 +1325,8 @@ def planning(): WHERE j.id NOT IN (SELECT DISTINCT job_id FROM print_slots WHERE status NOT IN ('cancelled','failed')) ORDER BY j.created_at DESC LIMIT 20''').fetchall() blocks = conn.execute( - "SELECT * FROM calendar_blocks WHERE date >= date('now') ORDER BY date LIMIT 60" + "SELECT * FROM calendar_blocks" + " WHERE COALESCE(date_end, date) >= date('now') ORDER BY date LIMIT 60" ).fetchall() schedule = conn.execute('SELECT * FROM working_schedule ORDER BY day_of_week').fetchall() conn.close() @@ -1384,12 +1401,18 @@ def working_schedule(): def calendar_blocks(): conn = get_db() if request.method == 'POST': - conn.execute('INSERT INTO calendar_blocks(date,type,printer_id,notes) VALUES(?,?,?,?)', ( - request.form['date'], - request.form.get('type','off'), - request.form.get('printer_id') or None, - request.form.get('notes','') - )) + date_start = request.form['date_start'] + date_end = request.form.get('date_end','').strip() or None + # If date_end < date_start, ignore it + if date_end and date_end < date_start: + date_end = None + conn.execute( + 'INSERT INTO calendar_blocks(date,date_end,type,printer_id,notes) VALUES(?,?,?,?,?)', + (date_start, date_end, + request.form.get('type','off'), + request.form.get('printer_id') or None, + request.form.get('notes','')) + ) conn.commit() blocks = conn.execute("SELECT cb.*, p.name as printer_name FROM calendar_blocks cb LEFT JOIN printers p ON cb.printer_id=p.id ORDER BY cb.date DESC LIMIT 50").fetchall() printers = conn.execute('SELECT * FROM printers ORDER BY id').fetchall() @@ -1837,7 +1860,10 @@ def api_availability(): for offset in range(28): d = today + timedelta(days=offset) date_str = str(d) - block = next((b for b in blocks if b['date'] == date_str and b['printer_id'] is None), None) + block = next((b for b in blocks + if b['date'] <= date_str + and (b.get('date_end') or b['date']) >= date_str + and b['printer_id'] is None), None) mode = block['type'] if block else schedule[d.weekday()]['mode'] if mode == 'off': result.append({ @@ -1930,8 +1956,9 @@ def calendar_ics(): # Jours indisponibles / bloquΓ©s for b in blocks: date_val = b['date'].replace('-', '') - # Date suivante pour DTEND all-day - d_obj = datetime.strptime(b['date'], '%Y-%m-%d') + timedelta(days=1) + # For DTEND: use date_end+1 if set, else date+1 (all-day exclusive end) + end_src = b['date_end'] if b['date_end'] else b['date'] + d_obj = datetime.strptime(end_src, '%Y-%m-%d') + timedelta(days=1) end_val = d_obj.strftime('%Y%m%d') label_map = {'off': 'πŸ”΄ Jour off', 'maintenance': 'πŸ”§ Maintenance', 'other': 'πŸ“Œ BloquΓ©'} label = label_map.get(b['type'], b['type']) diff --git a/templates/job_detail.html b/templates/job_detail.html index dfa7f54..ede3bda 100644 --- a/templates/job_detail.html +++ b/templates/job_detail.html @@ -250,6 +250,141 @@ + +{% set plates_done = slots | selectattr('status','in',['done']) | list | length %} +{% set plates_running = slots | selectattr('status','equalto','running') | list | length %} +{% set plates_planned = slots | selectattr('status','in',['planned','running']) | list | length %} +{% set plates_total = slots | rejectattr('status','in',['cancelled']) | list | length %} +{% set missing_plates = [total_plates_needed - plates_total, 0] | max %} + +
+
+
+
+ Plateaux d'impression +
+ + {% if total_plates_needed > 1 %} + + {{ plates_done }}/{{ total_plates_needed }} terminΓ©{{ 's' if plates_done > 1 else '' }} + + {% endif %} + {% if missing_plates > 0 %} + + {{ missing_plates }} plateau{{ 'x' if missing_plates > 1 else '' }} Γ  planifier + + {% endif %} + + Planning + +
+
+ + {% if slots %} +
+ + + + + + + + + + + + {% for s in slots %} + {% if s.status != 'cancelled' %} + + + + + + + + + {% if s.status == 'running' and s.ha_entity_prefix %} + + + + {% endif %} + {% endif %} + {% endfor %} + +
#DébutMachinePiècesStatut / Progression
{{ loop.index }} + {{ s.planned_start[:16].replace('T',' ') }} + {{ s.printer_name }} + {% set ep = s.effective_pieces %} + {% set ig = s.pieces_ignored %} + {% if ig > 0 %} + {{ ep - ig }}/{{ ep }} + ({{ ig }} ignorΓ©e{{ 's' if ig > 1 }}) + {% else %} + {{ ep }} + {% endif %} + + {% set sc = {'planned':'secondary','running':'primary','done':'success','failed':'danger'} %} + {% set sl = {'planned':'PlanifiΓ©','running':'En cours','done':'TerminΓ©','failed':'Γ‰chec'} %} + {{ sl.get(s.status, s.status) }} + {% if s.status == 'running' and s.ha_entity_prefix %} + + + + + {% endif %} + {% if s.failure_note %} +
{{ s.failure_note }} + {% endif %} +
+
+
+
+
+
+
+ {% else %} +
+ + Aucun crΓ©neau planifiΓ© pour cette commande. +
+ Aller au planning + +
+ {% endif %} + + + {% if total_plates_needed > 1 %} + + {% endif %} + +
+
+
+