1127 lines
46 KiB
Python
1127 lines
46 KiB
Python
"""
|
||
WDM-Performance – Produktionserfassung für ca. 40 Maschinen.
|
||
|
||
Reines Server-Side-Rendering (Flask + Jinja2, normale HTML-Formulare,
|
||
Post-Redirect-Get). Kein Client-seitiges JavaScript.
|
||
|
||
URL-Schema (statt der ursprünglich vorgesehenen Hash-Fragmente, die
|
||
technisch nur mit JavaScript funktionieren, als echte Server-Pfade):
|
||
/m<N> -> Maschinenseite (kurze Form, z.B. auf Tablets als
|
||
Startseite hinterlegt)
|
||
/m<N>_m_view_main -> dieselbe Maschinenseite, alte/lange URL-Form -
|
||
bleibt aus Kompatibilitätsgründen für bereits
|
||
eingerichtete Geräte/Lesezeichen weiter erreichbar
|
||
/start -> Produktionsleiter
|
||
"""
|
||
import os
|
||
import csv
|
||
import io
|
||
import time
|
||
import logging
|
||
import functools
|
||
|
||
import openpyxl
|
||
from flask import Flask, request, redirect, url_for, render_template, abort, session
|
||
|
||
import db
|
||
|
||
log = logging.getLogger("wdm-performance")
|
||
|
||
app = Flask(__name__)
|
||
app.secret_key = os.environ.get("SECRET_KEY", "bitte-in-der-.env-aendern")
|
||
# Upload-Größe für den Auftragsimport begrenzen (5 MB reichen für Excel-Listen locker aus).
|
||
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
|
||
|
||
MASCHINEN_PASSWORT = os.environ.get("MASCHINEN_PASSWORT", "admin")
|
||
|
||
try:
|
||
db.init_pool()
|
||
db.migrate()
|
||
except Exception:
|
||
log.error("Initialer DB-Verbindungsaufbau oder Migration fehlgeschlagen, wird bei erstem Request erneut versucht.")
|
||
|
||
|
||
def require_maschinen_auth(view):
|
||
"""Schützt die Maschinenverwaltung mit einem einfachen Passwort (Session-basiert)."""
|
||
@functools.wraps(view)
|
||
def wrapper(*args, **kwargs):
|
||
if not session.get("maschinen_auth"):
|
||
return redirect(url_for("maschinenverwaltung_login", next=request.path))
|
||
return view(*args, **kwargs)
|
||
return wrapper
|
||
|
||
|
||
STATUS_LABEL = {
|
||
"offen": "Offen",
|
||
"aktiv": "Aktiv",
|
||
"pause": "Pausiert",
|
||
"abgeschlossen": "Abgeschlossen",
|
||
}
|
||
STATUS_OPTIONEN = [("offen", "Offen"), ("aktiv", "Aktiv"), ("pause", "Pausiert"), ("abgeschlossen", "Abgeschlossen")]
|
||
|
||
# Anzahl Zeilen, auf die MDE-/Auftragslisten ausgelegt sind (kein hartes Limit,
|
||
# aber Referenzgröße für Layout/Performance-Überlegungen).
|
||
LISTEN_ERWARTETE_GROESSE = 100
|
||
MDE_LISTEN_LIMIT = 500
|
||
|
||
|
||
def find_abweichung_id_by_beschreibung(cur, text):
|
||
cur.execute('SELECT id FROM "abweichung" WHERE beschreibung = %s LIMIT 1', (text,))
|
||
row = cur.fetchone()
|
||
return str(row["id"]) if row else ""
|
||
|
||
|
||
def now_ts():
|
||
return int(time.time())
|
||
|
||
|
||
def fmt_num(v):
|
||
if v is None:
|
||
return ""
|
||
try:
|
||
f = float(v)
|
||
except (TypeError, ValueError):
|
||
return str(v)
|
||
return str(int(f)) if f == int(f) else f"{f:.2f}"
|
||
|
||
|
||
def fmt_ts(ts):
|
||
if not ts:
|
||
return "-"
|
||
try:
|
||
return time.strftime("%d.%m.%Y %H:%M", time.localtime(int(ts)))
|
||
except (TypeError, ValueError):
|
||
return "-"
|
||
|
||
|
||
# ---------------------------------------------------------------------
|
||
# DB-Hilfsfunktionen
|
||
# ---------------------------------------------------------------------
|
||
def get_maschine(cur, maschine_id):
|
||
cur.execute('SELECT * FROM "Maschinen" WHERE id = %s', (maschine_id,))
|
||
return cur.fetchone()
|
||
|
||
|
||
def get_aktiver_auftrag(cur, maschine_id):
|
||
cur.execute(
|
||
'SELECT * FROM "Auftraege" WHERE maschine_id = %s AND aktiv = true '
|
||
'ORDER BY id DESC LIMIT 1',
|
||
(maschine_id,),
|
||
)
|
||
return cur.fetchone()
|
||
|
||
|
||
def get_letzte_produktion(cur, maschine_id, auftragsnummer):
|
||
cur.execute(
|
||
'SELECT produziert FROM "MDE" WHERE maschine_id = %s AND "order" = %s '
|
||
'ORDER BY timestamp DESC, id DESC LIMIT 1',
|
||
(maschine_id, auftragsnummer),
|
||
)
|
||
row = cur.fetchone()
|
||
return row["produziert"] if row and row["produziert"] is not None else 0
|
||
|
||
|
||
def get_start_timestamp(cur, maschine_id, auftragsnummer):
|
||
cur.execute(
|
||
'SELECT timestamp FROM "MDE" WHERE maschine_id = %s AND "order" = %s '
|
||
'AND status = \'start\' ORDER BY timestamp ASC LIMIT 1',
|
||
(maschine_id, auftragsnummer),
|
||
)
|
||
row = cur.fetchone()
|
||
return row["timestamp"] if row else None
|
||
|
||
|
||
def berechne_h_avg(cur, maschine_id, auftragsnummer, produziert):
|
||
start_ts = get_start_timestamp(cur, maschine_id, auftragsnummer)
|
||
if not start_ts:
|
||
return 0
|
||
stunden = max((now_ts() - start_ts) / 3600.0, 1 / 60.0)
|
||
try:
|
||
return round(float(produziert) / stunden, 2)
|
||
except (TypeError, ZeroDivisionError):
|
||
return 0
|
||
|
||
|
||
def insert_mde(cur, maschine_id, auftragsnummer, produziert, status, p_grund=None):
|
||
h_avg = berechne_h_avg(cur, maschine_id, auftragsnummer, produziert)
|
||
cur.execute(
|
||
'INSERT INTO "MDE" (maschine_id, "timestamp", "order", produziert, status, h_date, h_avg, p_grund) '
|
||
"VALUES (%s, %s, %s, %s, %s, now(), %s, %s)",
|
||
(maschine_id, now_ts(), auftragsnummer, produziert, status, h_avg, p_grund),
|
||
)
|
||
|
||
|
||
def insert_mde_ohne_rate(cur, maschine_id, auftragsnummer, produziert, status, p_grund=None):
|
||
"""Wie insert_mde(), aber ohne Stück/Std-Berechnung - für nachträglich per
|
||
Import erzeugte Buchungen, für die es keinen echten Zeitverlauf gibt
|
||
(würde sonst einen irreführenden Momentan-Ausreißer bei h_avg erzeugen)."""
|
||
cur.execute(
|
||
'INSERT INTO "MDE" (maschine_id, "timestamp", "order", produziert, status, h_date, h_avg, p_grund) '
|
||
"VALUES (%s, %s, %s, %s, %s, now(), 0, %s)",
|
||
(maschine_id, now_ts(), auftragsnummer, produziert, status, p_grund),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------
|
||
# Auftragsimport (Excel)
|
||
# ---------------------------------------------------------------------
|
||
def _normalisiere_header(text):
|
||
return (str(text) if text is not None else "").strip().lower()
|
||
|
||
|
||
def _spalten_zuordnen(header_zeile):
|
||
"""Ordnet die Spalten der hochgeladenen Excel-Datei anhand von
|
||
Stichworten in der Kopfzeile zu (statt exakter Namen), damit z.B.
|
||
Zusatzhinweise wie "Artikel Übernehmen + Anzeigen" trotzdem erkannt
|
||
werden. Bezeichnung/Auftrag/Lieferdatum/Bemerkung werden bewusst nicht
|
||
zugeordnet (=ignoriert)."""
|
||
zuordnung = {}
|
||
for idx, wert in enumerate(header_zeile):
|
||
h = _normalisiere_header(wert)
|
||
if not h:
|
||
continue
|
||
if "maschine" in h:
|
||
zuordnung.setdefault("maschine", idx)
|
||
elif "prio" in h:
|
||
zuordnung.setdefault("prio", idx)
|
||
elif "fa" in h and "nr" in h:
|
||
zuordnung.setdefault("auftragsnummer", idx)
|
||
elif "rest" in h and "menge" in h:
|
||
zuordnung.setdefault("rest_menge", idx)
|
||
elif "ist" in h and "menge" in h:
|
||
zuordnung.setdefault("ist_menge", idx)
|
||
elif "artikel" in h:
|
||
zuordnung.setdefault("artikel", idx)
|
||
elif "soll" in h and ("stunde" in h or "/h" in h or "-h" in h or h.endswith("h")):
|
||
zuordnung.setdefault("soll_h", idx)
|
||
return zuordnung
|
||
|
||
|
||
def _zellwert_als_text(wert):
|
||
if wert is None:
|
||
return ""
|
||
if isinstance(wert, float) and wert.is_integer():
|
||
return str(int(wert))
|
||
return str(wert).strip()
|
||
|
||
|
||
def _zellwert_als_float(wert, default=0.0):
|
||
if wert is None or wert == "":
|
||
return default
|
||
if isinstance(wert, (int, float)):
|
||
return float(wert)
|
||
text = str(wert).strip()
|
||
if not text:
|
||
return default
|
||
# Deutsche Zahlformate aus CSV-Dateien abfangen, z.B. "1.234,56" oder "15,1".
|
||
if "," in text and "." in text:
|
||
text = text.replace(".", "").replace(",", ".")
|
||
elif "," in text:
|
||
text = text.replace(",", ".")
|
||
try:
|
||
return float(text)
|
||
except (TypeError, ValueError):
|
||
return default
|
||
|
||
|
||
def _lese_tabellen_zeilen(datei):
|
||
"""Liest eine hochgeladene .xlsx- oder .csv-Datei ein und liefert eine
|
||
Liste von Zeilen (jeweils eine Liste von Zellwerten). Bei CSV werden
|
||
Zeichenkodierung (utf-8/BOM, sonst cp1252) und Trennzeichen (Komma,
|
||
Semikolon - wie bei deutschem Excel-Export üblich - oder Tab) automatisch
|
||
erkannt."""
|
||
name = (datei.filename or "").lower()
|
||
if name.endswith(".xlsx"):
|
||
wb = openpyxl.load_workbook(datei.stream, data_only=True, read_only=True)
|
||
ws = wb.worksheets[0]
|
||
return [list(zeile) for zeile in ws.iter_rows(values_only=True)]
|
||
|
||
if name.endswith(".csv"):
|
||
rohdaten = datei.stream.read()
|
||
text = None
|
||
for encoding in ("utf-8-sig", "cp1252", "latin-1"):
|
||
try:
|
||
text = rohdaten.decode(encoding)
|
||
break
|
||
except UnicodeDecodeError:
|
||
continue
|
||
if text is None:
|
||
text = rohdaten.decode("utf-8", errors="replace")
|
||
|
||
zeilen_text = text.splitlines()
|
||
erste_zeile = zeilen_text[0] if zeilen_text else ""
|
||
try:
|
||
dialekt = csv.Sniffer().sniff(erste_zeile, delimiters=";,\t")
|
||
reader = csv.reader(io.StringIO(text), dialect=dialekt)
|
||
except csv.Error:
|
||
delimiter = ";" if erste_zeile.count(";") >= erste_zeile.count(",") else ","
|
||
reader = csv.reader(io.StringIO(text), delimiter=delimiter)
|
||
return [zeile for zeile in reader]
|
||
|
||
raise ValueError("Nicht unterstütztes Dateiformat.")
|
||
|
||
|
||
def validiere_menge(wert, bisherige_menge, soll):
|
||
if wert is None or wert == "":
|
||
return "Wert fehlt."
|
||
try:
|
||
wert = float(wert)
|
||
except (TypeError, ValueError):
|
||
return "Wert muss numerisch sein."
|
||
if wert < 0:
|
||
return "Wert darf nicht negativ sein."
|
||
if wert < float(bisherige_menge or 0):
|
||
return f"Wert darf nicht unter der bisher gemeldeten Menge ({fmt_num(bisherige_menge)}) liegen."
|
||
if soll and wert > float(soll) * 1.10:
|
||
return f"Wert darf maximal 10% über dem Sollwert ({fmt_num(soll)}) liegen."
|
||
return None
|
||
|
||
|
||
# ---------------------------------------------------------------------
|
||
# Maschinenseite
|
||
# ---------------------------------------------------------------------
|
||
@app.route("/m<int:maschine_id>")
|
||
@app.route("/m<int:maschine_id>_m_view_main", endpoint="machine_main_legacy")
|
||
def machine_main(maschine_id):
|
||
with db.get_cursor() as cur:
|
||
maschine = get_maschine(cur, maschine_id)
|
||
if not maschine:
|
||
abort(404)
|
||
auftrag = get_aktiver_auftrag(cur, maschine_id)
|
||
bisherige = 0
|
||
unter_soll = False
|
||
if auftrag:
|
||
bisherige = get_letzte_produktion(cur, maschine_id, auftrag["auftragsnummer"])
|
||
unter_soll = float(bisherige or 0) < float(auftrag["einheiten"] or 0)
|
||
|
||
soll = auftrag["einheiten"] if auftrag else 0
|
||
fortschritt = 0
|
||
if auftrag and soll:
|
||
fortschritt = min(100, round(float(bisherige) / float(soll) * 100))
|
||
|
||
return render_template(
|
||
"machine_main.html",
|
||
title=maschine["name"] or f"Maschine {maschine_id}",
|
||
subtitle=maschine["standort"] or "",
|
||
maschine=maschine,
|
||
auftrag=auftrag,
|
||
status_label=STATUS_LABEL.get(auftrag["status"], auftrag["status"]) if auftrag else "",
|
||
soll_fmt=fmt_num(soll),
|
||
rate_fmt=fmt_num(auftrag["soll_h"]) if auftrag else "",
|
||
bisherige_fmt=fmt_num(bisherige),
|
||
fortschritt=fortschritt,
|
||
unter_soll=unter_soll,
|
||
)
|
||
|
||
|
||
@app.route("/m<int:maschine_id>_auswahl")
|
||
def machine_auswahl(maschine_id):
|
||
with db.get_cursor() as cur:
|
||
maschine = get_maschine(cur, maschine_id)
|
||
if not maschine:
|
||
abort(404)
|
||
cur.execute(
|
||
'SELECT * FROM "Auftraege" WHERE maschine_id = %s AND status IN (\'offen\', \'pause\') '
|
||
'ORDER BY sid ASC NULLS LAST, id ASC',
|
||
(maschine_id,),
|
||
)
|
||
auftraege = cur.fetchall()
|
||
for a in auftraege:
|
||
a["einheiten_fmt"] = fmt_num(a["einheiten"])
|
||
a["status_label"] = STATUS_LABEL.get(a["status"], a["status"])
|
||
|
||
return render_template(
|
||
"machine_auswahl.html",
|
||
title="Auftragsauswahl",
|
||
subtitle=maschine["name"] or "",
|
||
maschine=maschine,
|
||
auftraege=auftraege,
|
||
)
|
||
|
||
|
||
@app.route("/m<int:maschine_id>_auswahl/waehlen", methods=["POST"])
|
||
def machine_auswahl_waehlen(maschine_id):
|
||
auftrag_id = request.form.get("auftrag_id")
|
||
if not auftrag_id:
|
||
abort(400)
|
||
with db.get_cursor() as cur:
|
||
cur.execute('SELECT * FROM "Auftraege" WHERE id = %s AND maschine_id = %s', (auftrag_id, maschine_id))
|
||
auftrag = cur.fetchone()
|
||
if not auftrag or auftrag["status"] not in ("offen", "pause"):
|
||
return redirect(url_for("machine_auswahl", maschine_id=maschine_id))
|
||
|
||
bestehender = get_aktiver_auftrag(cur, maschine_id)
|
||
if bestehender:
|
||
return redirect(url_for("machine_main", maschine_id=maschine_id))
|
||
|
||
ts = now_ts()
|
||
cur.execute(
|
||
'UPDATE "Auftraege" SET aktiv = true, status = \'aktiv\', '
|
||
'start = COALESCE(start, %s) WHERE id = %s',
|
||
(ts, auftrag_id),
|
||
)
|
||
bisherige = get_letzte_produktion(cur, maschine_id, auftrag["auftragsnummer"])
|
||
insert_mde(cur, maschine_id, auftrag["auftragsnummer"], bisherige, "start")
|
||
|
||
return redirect(url_for("machine_main", maschine_id=maschine_id))
|
||
|
||
|
||
def _meldung_view(maschine_id, ziel_status, eingabe_einheiten=None, eingabe_abweichung_id=None, error=None):
|
||
with db.get_cursor() as cur:
|
||
maschine = get_maschine(cur, maschine_id)
|
||
if not maschine:
|
||
abort(404)
|
||
auftrag = get_aktiver_auftrag(cur, maschine_id)
|
||
if not auftrag:
|
||
return render_template(
|
||
"machine_main.html", title=maschine["name"] or f"Maschine {maschine_id}",
|
||
subtitle=maschine["standort"] or "", maschine=maschine, auftrag=None,
|
||
error="Kein aktiver Auftrag.",
|
||
)
|
||
bisherige = get_letzte_produktion(cur, maschine_id, auftrag["auftragsnummer"])
|
||
cur.execute('SELECT * FROM "abweichung" ORDER BY beschreibung ASC')
|
||
abweichungen = cur.fetchall()
|
||
|
||
titel = "Auftrag pausieren" if ziel_status == "pause" else "Produktion erfassen"
|
||
form_action = url_for("machine_pause" if ziel_status == "pause" else "machine_erfassen", maschine_id=maschine_id)
|
||
|
||
if eingabe_abweichung_id is None:
|
||
default_text = "Pause" if ziel_status == "pause" else "Mengenmeldung"
|
||
eingabe_abweichung_id = find_abweichung_id_by_beschreibung(cur, default_text)
|
||
|
||
return render_template(
|
||
"machine_meldung.html",
|
||
title=titel,
|
||
subtitle=maschine["name"] or "",
|
||
maschine=maschine,
|
||
bisherige_fmt=fmt_num(bisherige),
|
||
soll_fmt=fmt_num(auftrag["einheiten"]),
|
||
abweichungen=abweichungen,
|
||
eingabe_einheiten=eingabe_einheiten if eingabe_einheiten is not None else "",
|
||
eingabe_abweichung_id=eingabe_abweichung_id or "",
|
||
form_action=form_action,
|
||
error=error,
|
||
)
|
||
|
||
|
||
def _meldung_speichern(maschine_id, ziel_status):
|
||
einheiten = request.form.get("einheiten")
|
||
abweichung_id = request.form.get("abweichung_id")
|
||
|
||
with db.get_cursor() as cur:
|
||
auftrag = get_aktiver_auftrag(cur, maschine_id)
|
||
if not auftrag:
|
||
return redirect(url_for("machine_main", maschine_id=maschine_id))
|
||
|
||
if not abweichung_id:
|
||
return _meldung_view(maschine_id, ziel_status, einheiten, abweichung_id, "Abweichungsgrund muss ausgewählt werden.")
|
||
|
||
cur.execute('SELECT * FROM "abweichung" WHERE id = %s', (abweichung_id,))
|
||
abweichung = cur.fetchone()
|
||
if not abweichung:
|
||
return _meldung_view(maschine_id, ziel_status, einheiten, abweichung_id, "Ungültiger Abweichungsgrund.")
|
||
|
||
bisherige = get_letzte_produktion(cur, maschine_id, auftrag["auftragsnummer"])
|
||
fehler = validiere_menge(einheiten, bisherige, auftrag["einheiten"])
|
||
if fehler:
|
||
return _meldung_view(maschine_id, ziel_status, einheiten, abweichung_id, fehler)
|
||
|
||
einheiten_f = float(einheiten)
|
||
insert_mde(cur, maschine_id, auftrag["auftragsnummer"], einheiten_f, ziel_status, abweichung["beschreibung"])
|
||
|
||
if ziel_status == "pause":
|
||
cur.execute(
|
||
'UPDATE "Auftraege" SET status = \'pause\', aktiv = false WHERE id = %s',
|
||
(auftrag["id"],),
|
||
)
|
||
|
||
return redirect(url_for("machine_main", maschine_id=maschine_id))
|
||
|
||
|
||
@app.route("/m<int:maschine_id>_erfassen", methods=["GET", "POST"])
|
||
def machine_erfassen(maschine_id):
|
||
if request.method == "POST":
|
||
return _meldung_speichern(maschine_id, "produziert")
|
||
return _meldung_view(maschine_id, "produziert")
|
||
|
||
|
||
@app.route("/m<int:maschine_id>_pause", methods=["GET", "POST"])
|
||
def machine_pause(maschine_id):
|
||
if request.method == "POST":
|
||
return _meldung_speichern(maschine_id, "pause")
|
||
return _meldung_view(maschine_id, "pause")
|
||
|
||
|
||
@app.route("/m<int:maschine_id>_beenden_confirm")
|
||
def machine_beenden_confirm(maschine_id):
|
||
with db.get_cursor() as cur:
|
||
maschine = get_maschine(cur, maschine_id)
|
||
if not maschine:
|
||
abort(404)
|
||
auftrag = get_aktiver_auftrag(cur, maschine_id)
|
||
if not auftrag:
|
||
return redirect(url_for("machine_main", maschine_id=maschine_id))
|
||
bisherige = get_letzte_produktion(cur, maschine_id, auftrag["auftragsnummer"])
|
||
return render_template(
|
||
"machine_beenden_confirm.html",
|
||
title="Auftrag beenden",
|
||
subtitle=maschine["name"] or "",
|
||
maschine=maschine,
|
||
soll_fmt=fmt_num(auftrag["einheiten"]),
|
||
bisherige_fmt=fmt_num(bisherige),
|
||
)
|
||
|
||
|
||
@app.route("/m<int:maschine_id>_beenden", methods=["POST"])
|
||
def machine_beenden(maschine_id):
|
||
with db.get_cursor() as cur:
|
||
auftrag = get_aktiver_auftrag(cur, maschine_id)
|
||
if not auftrag:
|
||
return redirect(url_for("machine_main", maschine_id=maschine_id))
|
||
|
||
soll = auftrag["einheiten"] or 0
|
||
insert_mde(cur, maschine_id, auftrag["auftragsnummer"], soll, "beendet")
|
||
cur.execute(
|
||
'UPDATE "Auftraege" SET status = \'abgeschlossen\', aktiv = false, ende = %s WHERE id = %s',
|
||
(now_ts(), auftrag["id"]),
|
||
)
|
||
|
||
return redirect(url_for("machine_main", maschine_id=maschine_id))
|
||
|
||
|
||
# ---------------------------------------------------------------------
|
||
# Produktionsleiter
|
||
# ---------------------------------------------------------------------
|
||
@app.route("/start")
|
||
def leitstand_home():
|
||
return render_template("leitstand_home.html", title="Produktionsleiter", subtitle="Übersicht & Steuerung")
|
||
|
||
|
||
@app.route("/start_maschinen")
|
||
def leitstand_maschinen():
|
||
with db.get_cursor() as cur:
|
||
maschinen = get_maschinen_liste(cur)
|
||
for m in maschinen:
|
||
aktiver_auftrag = get_aktiver_auftrag(cur, m["id"])
|
||
m["hat_aktiven_auftrag"] = aktiver_auftrag is not None
|
||
m["aktiver_artikel"] = aktiver_auftrag["artikel"] if aktiver_auftrag else None
|
||
return render_template(
|
||
"leitstand_maschinen.html", title="Maschinenübersicht", subtitle="Direkt zu einer Maschinenseite springen",
|
||
maschinen=maschinen,
|
||
)
|
||
|
||
|
||
@app.route("/start_maschinen_verwaltung/login", methods=["GET", "POST"])
|
||
def maschinenverwaltung_login():
|
||
next_url = request.args.get("next") or request.form.get("next") or url_for("maschinenverwaltung")
|
||
if request.method == "POST":
|
||
passwort = request.form.get("passwort") or ""
|
||
if passwort == MASCHINEN_PASSWORT:
|
||
session["maschinen_auth"] = True
|
||
return redirect(next_url)
|
||
return render_template("maschinenverwaltung_login.html", title="Maschinenverwaltung", next=next_url, error="Falsches Passwort.")
|
||
return render_template("maschinenverwaltung_login.html", title="Maschinenverwaltung", next=next_url)
|
||
|
||
|
||
@app.route("/start_maschinen_verwaltung/logout", methods=["POST"])
|
||
def maschinenverwaltung_logout():
|
||
session.pop("maschinen_auth", None)
|
||
return redirect(url_for("leitstand_home"))
|
||
|
||
|
||
@app.route("/start_maschinen_verwaltung")
|
||
@require_maschinen_auth
|
||
def maschinenverwaltung():
|
||
with db.get_cursor() as cur:
|
||
maschinen = get_maschinen_liste(cur)
|
||
return render_template(
|
||
"maschinenverwaltung.html", title="Maschinenverwaltung", subtitle="Maschinen anlegen, bearbeiten, löschen",
|
||
maschinen=maschinen,
|
||
)
|
||
|
||
|
||
@app.route("/start_maschinen_verwaltung/neu", methods=["GET", "POST"])
|
||
@require_maschinen_auth
|
||
def maschinenverwaltung_neu():
|
||
if request.method == "POST":
|
||
name = (request.form.get("name") or "").strip()
|
||
standort = (request.form.get("standort") or "").strip()
|
||
if not name:
|
||
return render_template(
|
||
"maschinenverwaltung_bearbeiten.html", title="Maschine anlegen",
|
||
maschine={"id": None, "name": name, "standort": standort}, neu=True,
|
||
error="Name darf nicht leer sein.",
|
||
)
|
||
with db.get_cursor() as cur:
|
||
cur.execute('INSERT INTO "Maschinen" (name, standort) VALUES (%s, %s)', (name, standort or None))
|
||
return redirect(url_for("maschinenverwaltung"))
|
||
|
||
return render_template("maschinenverwaltung_bearbeiten.html", title="Maschine anlegen", maschine={"id": None, "name": "", "standort": ""}, neu=True)
|
||
|
||
|
||
@app.route("/start_maschinen_verwaltung/<int:maschine_id>/bearbeiten", methods=["GET", "POST"])
|
||
@require_maschinen_auth
|
||
def maschinenverwaltung_bearbeiten(maschine_id):
|
||
with db.get_cursor() as cur:
|
||
if request.method == "POST":
|
||
name = (request.form.get("name") or "").strip()
|
||
standort = (request.form.get("standort") or "").strip()
|
||
if not name:
|
||
cur.execute('SELECT * FROM "Maschinen" WHERE id = %s', (maschine_id,))
|
||
maschine = cur.fetchone()
|
||
return render_template(
|
||
"maschinenverwaltung_bearbeiten.html", title="Maschine bearbeiten",
|
||
maschine=maschine, neu=False, error="Name darf nicht leer sein.",
|
||
)
|
||
cur.execute('UPDATE "Maschinen" SET name = %s, standort = %s WHERE id = %s', (name, standort or None, maschine_id))
|
||
return redirect(url_for("maschinenverwaltung"))
|
||
|
||
cur.execute('SELECT * FROM "Maschinen" WHERE id = %s', (maschine_id,))
|
||
maschine = cur.fetchone()
|
||
if not maschine:
|
||
abort(404)
|
||
return render_template("maschinenverwaltung_bearbeiten.html", title="Maschine bearbeiten", maschine=maschine, neu=False)
|
||
|
||
|
||
@app.route("/start_maschinen_verwaltung/<int:maschine_id>/loeschen", methods=["POST"])
|
||
@require_maschinen_auth
|
||
def maschinenverwaltung_loeschen(maschine_id):
|
||
with db.get_cursor() as cur:
|
||
cur.execute('SELECT COUNT(*) AS n FROM "Auftraege" WHERE maschine_id = %s', (maschine_id,))
|
||
anzahl_auftraege = cur.fetchone()["n"]
|
||
if anzahl_auftraege:
|
||
maschinen = get_maschinen_liste(cur)
|
||
return render_template(
|
||
"maschinenverwaltung.html", title="Maschinenverwaltung", subtitle="Maschinen anlegen, bearbeiten, löschen",
|
||
maschinen=maschinen,
|
||
error=f"Maschine kann nicht gelöscht werden: es existieren noch {int(anzahl_auftraege)} zugehörige Aufträge.",
|
||
)
|
||
cur.execute('DELETE FROM "Maschinen" WHERE id = %s', (maschine_id,))
|
||
return redirect(url_for("maschinenverwaltung"))
|
||
|
||
|
||
def get_maschinen_liste(cur):
|
||
cur.execute('SELECT * FROM "Maschinen" ORDER BY id ASC')
|
||
return cur.fetchall()
|
||
|
||
|
||
@app.route("/start_anlegen", methods=["GET", "POST"])
|
||
def leitstand_anlegen():
|
||
with db.get_cursor() as cur:
|
||
maschinen = get_maschinen_liste(cur)
|
||
|
||
if request.method == "GET":
|
||
return render_template("leitstand_anlegen.html", title="Auftrag anlegen", maschinen=maschinen, eingabe={})
|
||
|
||
eingabe = {
|
||
"maschine_id": request.form.get("maschine_id"),
|
||
"auftragsnummer": (request.form.get("auftragsnummer") or "").strip(),
|
||
"einheiten": request.form.get("einheiten"),
|
||
"soll_h": request.form.get("soll_h"),
|
||
"artikel": (request.form.get("artikel") or "").strip(),
|
||
"prioritaet": (request.form.get("prioritaet") or "").strip(),
|
||
}
|
||
|
||
maschine_id = eingabe["maschine_id"]
|
||
auftragsnummer = eingabe["auftragsnummer"]
|
||
error = None
|
||
einheiten_f = soll_h_f = None
|
||
position = None
|
||
|
||
if not maschine_id:
|
||
error = "Maschine muss ausgewählt werden."
|
||
elif not auftragsnummer:
|
||
error = "Auftragsnummer darf nicht leer sein."
|
||
else:
|
||
try:
|
||
einheiten_f = float(eingabe["einheiten"])
|
||
soll_h_f = float(eingabe["soll_h"])
|
||
except (TypeError, ValueError):
|
||
error = "Produktionsmenge und Stück/Stunde müssen numerisch sein."
|
||
else:
|
||
if einheiten_f <= 0 or soll_h_f <= 0:
|
||
error = "Produktionsmenge und Stück/Stunde müssen positiv sein."
|
||
|
||
if not error and eingabe["prioritaet"]:
|
||
try:
|
||
position = int(eingabe["prioritaet"])
|
||
except ValueError:
|
||
error = "Priorität muss eine ganze Zahl sein."
|
||
else:
|
||
if position < 1:
|
||
error = "Priorität muss mindestens 1 sein."
|
||
|
||
if error:
|
||
return render_template("leitstand_anlegen.html", title="Auftrag anlegen", maschinen=maschinen, eingabe=eingabe, error=error)
|
||
|
||
maschine = get_maschine(cur, maschine_id)
|
||
if not maschine:
|
||
return render_template("leitstand_anlegen.html", title="Auftrag anlegen", maschinen=maschinen, eingabe=eingabe, error="Maschine nicht gefunden.")
|
||
|
||
# Aktuelle Warteschlange (offen/pause) dieser Maschine ermitteln, um die
|
||
# gewünschte Priorität sauber einzureihen statt nur einen rohen sid-Wert
|
||
# zu setzen - bestehende Aufträge rücken dabei automatisch nach.
|
||
cur.execute(
|
||
'SELECT id FROM "Auftraege" WHERE maschine_id = %s AND status IN (\'offen\', \'pause\') '
|
||
'ORDER BY sid ASC NULLS LAST, id ASC',
|
||
(maschine_id,),
|
||
)
|
||
warteschlange = [row["id"] for row in cur.fetchall()]
|
||
anzahl = len(warteschlange)
|
||
position = min(position, anzahl + 1) if position else anzahl + 1
|
||
|
||
cur.execute(
|
||
'INSERT INTO "Auftraege" (auftragsnummer, einheiten, soll_h, status, aktiv, created_at, sid, maschine_id, artikel) '
|
||
"VALUES (%s, %s, %s, 'offen', false, %s, %s, %s, %s) RETURNING id",
|
||
(auftragsnummer, einheiten_f, soll_h_f, now_ts(), position, maschine_id, eingabe["artikel"] or None),
|
||
)
|
||
neuer_id = cur.fetchone()["id"]
|
||
|
||
warteschlange.insert(position - 1, neuer_id)
|
||
for idx, a_id in enumerate(warteschlange, start=1):
|
||
cur.execute('UPDATE "Auftraege" SET sid = %s WHERE id = %s', (idx, a_id))
|
||
|
||
# Maschinenauswahl bewusst erhalten (nicht zurücksetzen) - beim Anlegen
|
||
# mehrerer Aufträge nacheinander für dieselbe Maschine soll das
|
||
# Dropdown nicht bei jedem Speichern auf die erste Maschine der Liste
|
||
# zurückspringen.
|
||
return render_template(
|
||
"leitstand_anlegen.html", title="Auftrag anlegen", maschinen=maschinen,
|
||
eingabe={"maschine_id": maschine_id},
|
||
info=f"Auftrag {auftragsnummer} wurde an Position {position} angelegt.",
|
||
)
|
||
|
||
|
||
@app.route("/start_uebersicht")
|
||
def leitstand_uebersicht():
|
||
filter_maschine_id = request.args.get("maschine_id") or ""
|
||
filter_status = request.args.get("status", "offen_pause")
|
||
with db.get_cursor() as cur:
|
||
maschinen = get_maschinen_liste(cur)
|
||
|
||
bedingungen = []
|
||
params = []
|
||
if filter_maschine_id:
|
||
bedingungen.append('maschine_id = %s')
|
||
params.append(filter_maschine_id)
|
||
if filter_status == "offen_pause":
|
||
bedingungen.append("status IN ('offen', 'pause')")
|
||
|
||
where_sql = (" WHERE " + " AND ".join(bedingungen)) if bedingungen else ""
|
||
cur.execute(
|
||
f'SELECT * FROM "Auftraege"{where_sql} ORDER BY maschine_id ASC, sid ASC NULLS LAST, id DESC '
|
||
f'LIMIT {MDE_LISTEN_LIMIT}',
|
||
params,
|
||
)
|
||
auftraege = cur.fetchall()
|
||
maschine_name_by_id = {m["id"]: (m["name"] or f"Maschine {m['id']}") for m in maschinen}
|
||
for a in auftraege:
|
||
a["einheiten_fmt"] = fmt_num(a["einheiten"])
|
||
a["soll_h_fmt"] = fmt_num(a["soll_h"])
|
||
a["sid_fmt"] = fmt_num(a["sid"])
|
||
a["status_label"] = STATUS_LABEL.get(a["status"], a["status"])
|
||
a["maschine_name"] = maschine_name_by_id.get(int(a["maschine_id"])) if a["maschine_id"] is not None else ""
|
||
|
||
return render_template(
|
||
"leitstand_uebersicht.html",
|
||
title="Auftragsübersicht",
|
||
content_class="wide",
|
||
maschinen=maschinen,
|
||
auftraege=auftraege,
|
||
filter_maschine_id=filter_maschine_id,
|
||
filter_status=filter_status,
|
||
)
|
||
|
||
|
||
@app.route("/start_uebersicht/<int:auftrag_id>/bearbeiten", methods=["GET", "POST"])
|
||
def leitstand_uebersicht_bearbeiten(auftrag_id):
|
||
"""Auftrag bearbeiten. Status und Priorität (sid) sind hier bewusst NICHT
|
||
editierbar - Status ändert sich nur über die eigentlichen Maschinen-
|
||
Aktionen (Start/Pause/Beenden), Priorität nur über die Drag&Drop-Seite
|
||
"Auftragspriorität". Aktive Aufträge werden nur angezeigt, eine
|
||
Bearbeitung wird komplett verweigert (Fehlermeldung), da sie gerade an
|
||
der Maschine läuft."""
|
||
filter_maschine_id = request.args.get("maschine_id") or request.form.get("maschine_id_filter") or ""
|
||
filter_status = request.args.get("status") or request.form.get("status_filter") or "offen_pause"
|
||
with db.get_cursor() as cur:
|
||
maschinen = get_maschinen_liste(cur)
|
||
|
||
cur.execute('SELECT * FROM "Auftraege" WHERE id = %s', (auftrag_id,))
|
||
alt = cur.fetchone()
|
||
if not alt:
|
||
abort(404)
|
||
|
||
if alt["status"] == "aktiv":
|
||
alt["einheiten_fmt"] = fmt_num(alt["einheiten"])
|
||
alt["soll_h_fmt"] = fmt_num(alt["soll_h"])
|
||
alt["sid_fmt"] = fmt_num(alt["sid"])
|
||
return render_template(
|
||
"leitstand_uebersicht_bearbeiten.html", title="Auftrag bearbeiten",
|
||
auftrag=alt, maschinen=maschinen, gesperrt=True,
|
||
status_anzeige=STATUS_LABEL.get(alt["status"], alt["status"]),
|
||
filter_maschine_id=filter_maschine_id, filter_status=filter_status,
|
||
error="Dieser Auftrag ist aktuell aktiv und kann deshalb nicht bearbeitet werden. "
|
||
"Bitte an der Maschine erst pausieren oder beenden.",
|
||
)
|
||
|
||
if request.method == "POST":
|
||
auftragsnummer = (request.form.get("auftragsnummer") or "").strip()
|
||
einheiten = request.form.get("einheiten")
|
||
soll_h = request.form.get("soll_h")
|
||
artikel = (request.form.get("artikel") or "").strip() or None
|
||
neue_maschine_id = request.form.get("maschine_id")
|
||
|
||
error = None
|
||
einheiten_f = soll_h_f = None
|
||
try:
|
||
einheiten_f = float(einheiten)
|
||
soll_h_f = float(soll_h)
|
||
except (TypeError, ValueError):
|
||
error = "Menge und Stück/h müssen numerisch sein."
|
||
|
||
if not error and not auftragsnummer:
|
||
error = "Auftragsnummer darf nicht leer sein."
|
||
|
||
if not error:
|
||
if not neue_maschine_id:
|
||
error = "Maschine muss ausgewählt werden."
|
||
elif not get_maschine(cur, neue_maschine_id):
|
||
error = "Maschine nicht gefunden."
|
||
|
||
bisherige = 0
|
||
if not error:
|
||
bisherige = get_letzte_produktion(cur, alt["maschine_id"], alt["auftragsnummer"]) or 0
|
||
if bisherige and einheiten_f * 1.10 < float(bisherige):
|
||
mindest = float(bisherige) / 1.10
|
||
error = (
|
||
f"An diesem Auftrag wurde bereits gearbeitet ({fmt_num(bisherige)} St. gemeldet). "
|
||
f"Die Produktionsmenge darf nicht unter {fmt_num(mindest)} gesetzt werden, "
|
||
f"da die bereits gemeldete Menge sonst außerhalb der 110%-Toleranz läge."
|
||
)
|
||
|
||
if error:
|
||
auftrag = dict(alt)
|
||
auftrag["einheiten_fmt"] = einheiten
|
||
auftrag["soll_h_fmt"] = soll_h
|
||
auftrag["sid_fmt"] = fmt_num(alt["sid"])
|
||
auftrag["artikel"] = artikel
|
||
auftrag["maschine_id"] = neue_maschine_id or alt["maschine_id"]
|
||
return render_template(
|
||
"leitstand_uebersicht_bearbeiten.html", title="Auftrag bearbeiten",
|
||
auftrag=auftrag, maschinen=maschinen,
|
||
status_anzeige=STATUS_LABEL.get(alt["status"], alt["status"]),
|
||
filter_maschine_id=filter_maschine_id, filter_status=filter_status, error=error,
|
||
)
|
||
|
||
cur.execute(
|
||
'UPDATE "Auftraege" SET auftragsnummer = %s, einheiten = %s, soll_h = %s, '
|
||
'artikel = %s, maschine_id = %s WHERE id = %s',
|
||
(auftragsnummer, einheiten_f, soll_h_f, artikel, neue_maschine_id, auftrag_id),
|
||
)
|
||
# Maschine und/oder Auftragsnummer geändert? Dann muss die MDE-Historie
|
||
# (dort per maschine_id + Auftragsnummer verknüpft) mitgezogen werden.
|
||
if str(neue_maschine_id) != str(alt["maschine_id"]) or auftragsnummer != alt["auftragsnummer"]:
|
||
cur.execute(
|
||
'UPDATE "MDE" SET maschine_id = %s, "order" = %s WHERE maschine_id = %s AND "order" = %s',
|
||
(neue_maschine_id, auftragsnummer, alt["maschine_id"], alt["auftragsnummer"]),
|
||
)
|
||
return redirect(url_for("leitstand_uebersicht", maschine_id=filter_maschine_id, status=filter_status))
|
||
|
||
alt["einheiten_fmt"] = fmt_num(alt["einheiten"])
|
||
alt["soll_h_fmt"] = fmt_num(alt["soll_h"])
|
||
alt["sid_fmt"] = fmt_num(alt["sid"])
|
||
|
||
return render_template(
|
||
"leitstand_uebersicht_bearbeiten.html", title="Auftrag bearbeiten",
|
||
auftrag=alt, maschinen=maschinen,
|
||
status_anzeige=STATUS_LABEL.get(alt["status"], alt["status"]),
|
||
filter_maschine_id=filter_maschine_id, filter_status=filter_status,
|
||
)
|
||
|
||
|
||
@app.route("/start_uebersicht/<int:auftrag_id>/loeschen", methods=["POST"])
|
||
def leitstand_uebersicht_loeschen(auftrag_id):
|
||
filter_maschine_id = request.form.get("maschine_id") or ""
|
||
filter_status = request.form.get("status") or "offen_pause"
|
||
with db.get_cursor() as cur:
|
||
cur.execute('DELETE FROM "Auftraege" WHERE id = %s', (auftrag_id,))
|
||
return redirect(url_for("leitstand_uebersicht", maschine_id=filter_maschine_id, status=filter_status))
|
||
|
||
|
||
@app.route("/start_korrektur")
|
||
def leitstand_korrektur():
|
||
filter_maschine_id = request.args.get("maschine_id") or ""
|
||
with db.get_cursor() as cur:
|
||
maschinen = get_maschinen_liste(cur)
|
||
if filter_maschine_id:
|
||
cur.execute(f'SELECT * FROM "MDE" WHERE maschine_id = %s ORDER BY "timestamp" DESC, id DESC LIMIT {MDE_LISTEN_LIMIT}', (filter_maschine_id,))
|
||
else:
|
||
cur.execute(f'SELECT * FROM "MDE" ORDER BY "timestamp" DESC, id DESC LIMIT {MDE_LISTEN_LIMIT}')
|
||
eintraege = cur.fetchall()
|
||
for m in eintraege:
|
||
m["zeit_fmt"] = fmt_ts(m["timestamp"])
|
||
m["produziert_fmt"] = fmt_num(m["produziert"])
|
||
m["h_avg_fmt"] = fmt_num(m["h_avg"])
|
||
|
||
return render_template(
|
||
"leitstand_korrektur.html", title="Buchungskorrektur", subtitle="Direkter Eingriff in die MDE-Tabelle",
|
||
content_class="wide",
|
||
maschinen=maschinen, eintraege=eintraege, filter_maschine_id=filter_maschine_id,
|
||
)
|
||
|
||
|
||
@app.route("/start_korrektur/<int:mde_id>/bearbeiten", methods=["GET", "POST"])
|
||
def leitstand_korrektur_bearbeiten(mde_id):
|
||
filter_maschine_id = request.args.get("maschine_id") or request.form.get("maschine_id_filter") or ""
|
||
with db.get_cursor() as cur:
|
||
if request.method == "POST":
|
||
order = request.form.get("order")
|
||
produziert = request.form.get("produziert")
|
||
status = request.form.get("status")
|
||
h_avg = request.form.get("h_avg")
|
||
p_grund = request.form.get("p_grund") or None
|
||
|
||
try:
|
||
produziert_f = float(produziert) if produziert not in (None, "") else None
|
||
h_avg_f = float(h_avg) if h_avg not in (None, "") else None
|
||
except (TypeError, ValueError):
|
||
cur.execute('SELECT * FROM "MDE" WHERE id = %s', (mde_id,))
|
||
eintrag = cur.fetchone()
|
||
eintrag["produziert_fmt"] = produziert
|
||
eintrag["h_avg_fmt"] = h_avg
|
||
return render_template(
|
||
"leitstand_korrektur_bearbeiten.html", title="Buchung bearbeiten",
|
||
eintrag=eintrag, filter_maschine_id=filter_maschine_id,
|
||
error="Produziert und Ø/h müssen numerisch sein.",
|
||
)
|
||
|
||
cur.execute(
|
||
'UPDATE "MDE" SET "order" = %s, produziert = %s, status = %s, h_avg = %s, p_grund = %s WHERE id = %s',
|
||
(order, produziert_f, status, h_avg_f, p_grund, mde_id),
|
||
)
|
||
return redirect(url_for("leitstand_korrektur", maschine_id=filter_maschine_id))
|
||
|
||
cur.execute('SELECT * FROM "MDE" WHERE id = %s', (mde_id,))
|
||
eintrag = cur.fetchone()
|
||
if not eintrag:
|
||
abort(404)
|
||
eintrag["produziert_fmt"] = fmt_num(eintrag["produziert"])
|
||
eintrag["h_avg_fmt"] = fmt_num(eintrag["h_avg"])
|
||
|
||
return render_template(
|
||
"leitstand_korrektur_bearbeiten.html", title="Buchung bearbeiten",
|
||
eintrag=eintrag, filter_maschine_id=filter_maschine_id,
|
||
)
|
||
|
||
|
||
@app.route("/start_korrektur/<int:mde_id>/loeschen", methods=["POST"])
|
||
def leitstand_korrektur_loeschen(mde_id):
|
||
filter_maschine_id = request.form.get("maschine_id") or ""
|
||
with db.get_cursor() as cur:
|
||
cur.execute('DELETE FROM "MDE" WHERE id = %s', (mde_id,))
|
||
return redirect(url_for("leitstand_korrektur", maschine_id=filter_maschine_id))
|
||
|
||
|
||
@app.route("/start_prioritaet")
|
||
def leitstand_prioritaet():
|
||
filter_maschine_id = request.args.get("maschine_id") or ""
|
||
with db.get_cursor() as cur:
|
||
maschinen = get_maschinen_liste(cur)
|
||
if not filter_maschine_id and maschinen:
|
||
filter_maschine_id = str(maschinen[0]["id"])
|
||
|
||
auftraege = []
|
||
if filter_maschine_id:
|
||
cur.execute(
|
||
'SELECT * FROM "Auftraege" WHERE maschine_id = %s AND status IN (\'offen\', \'pause\') '
|
||
'ORDER BY sid ASC NULLS LAST, id ASC',
|
||
(filter_maschine_id,),
|
||
)
|
||
auftraege = cur.fetchall()
|
||
for a in auftraege:
|
||
a["status_label"] = STATUS_LABEL.get(a["status"], a["status"])
|
||
|
||
return render_template(
|
||
"leitstand_prioritaet.html", title="Auftragspriorität", subtitle="Reihenfolge je Maschine (sid)",
|
||
maschinen=maschinen, auftraege=auftraege, filter_maschine_id=filter_maschine_id,
|
||
)
|
||
|
||
|
||
@app.route("/start_prioritaet/reihenfolge", methods=["POST"])
|
||
def leitstand_prioritaet_reihenfolge():
|
||
"""Speichert eine per Drag & Drop auf der Prioritätsseite neu
|
||
festgelegte Reihenfolge. Erwartet JSON: {"maschine_id": ..., "ids": [...]}
|
||
(Auftrags-IDs in der gewünschten neuen Reihenfolge)."""
|
||
data = request.get_json(silent=True) or {}
|
||
maschine_id = data.get("maschine_id")
|
||
ids = data.get("ids") or []
|
||
if not maschine_id or not ids:
|
||
return {"status": "error", "message": "Ungültige Anfrage."}, 400
|
||
|
||
with db.get_cursor() as cur:
|
||
cur.execute(
|
||
'SELECT id FROM "Auftraege" WHERE maschine_id = %s AND status IN (\'offen\', \'pause\')',
|
||
(maschine_id,),
|
||
)
|
||
gueltige_ids = {row["id"] for row in cur.fetchall()}
|
||
try:
|
||
angefragte_ids = [int(i) for i in ids]
|
||
except (TypeError, ValueError):
|
||
return {"status": "error", "message": "Ungültige Auftrags-IDs."}, 400
|
||
|
||
neue_reihenfolge = [i for i in angefragte_ids if i in gueltige_ids]
|
||
for position, auftrag_id in enumerate(neue_reihenfolge, start=1):
|
||
cur.execute('UPDATE "Auftraege" SET sid = %s WHERE id = %s', (position, auftrag_id))
|
||
|
||
return {"status": "ok"}
|
||
|
||
|
||
@app.route("/start_import", methods=["GET", "POST"])
|
||
def leitstand_import():
|
||
if request.method == "GET":
|
||
return render_template("leitstand_import.html", title="Aufträge importieren")
|
||
|
||
datei = request.files.get("datei")
|
||
if not datei or not datei.filename:
|
||
return render_template("leitstand_import.html", title="Aufträge importieren",
|
||
error="Bitte eine Excel- oder CSV-Datei auswählen.")
|
||
if not datei.filename.lower().endswith((".xlsx", ".csv")):
|
||
return render_template("leitstand_import.html", title="Aufträge importieren",
|
||
error="Nur .xlsx- oder .csv-Dateien werden unterstützt.")
|
||
|
||
try:
|
||
zeilen = _lese_tabellen_zeilen(datei)
|
||
except Exception as exc:
|
||
log.error("Import: Datei konnte nicht gelesen werden: %s", exc)
|
||
return render_template("leitstand_import.html", title="Aufträge importieren",
|
||
error="Datei konnte nicht gelesen werden. Ist es eine gültige .xlsx- oder .csv-Datei?")
|
||
|
||
if not zeilen:
|
||
return render_template("leitstand_import.html", title="Aufträge importieren",
|
||
error="Die Datei enthält keine Daten.")
|
||
|
||
spalten = _spalten_zuordnen(zeilen[0])
|
||
if "maschine" not in spalten or "auftragsnummer" not in spalten:
|
||
return render_template(
|
||
"leitstand_import.html", title="Aufträge importieren",
|
||
error='Spalten "Maschine" und "FA.-Nr." konnten in der Kopfzeile nicht gefunden werden.',
|
||
)
|
||
|
||
importiert = []
|
||
uebersprungen_vorhanden = []
|
||
verworfen_maschine = []
|
||
fehlerhafte_zeilen = []
|
||
|
||
with db.get_cursor() as cur:
|
||
maschinen = get_maschinen_liste(cur)
|
||
maschinen_by_name = {(m["name"] or "").strip().lower(): m for m in maschinen}
|
||
|
||
for zeilennummer, zeile in enumerate(zeilen[1:], start=2):
|
||
def wert(feld):
|
||
idx = spalten.get(feld)
|
||
return zeile[idx] if idx is not None and idx < len(zeile) else None
|
||
|
||
maschine_name = _zellwert_als_text(wert("maschine"))
|
||
auftragsnummer = _zellwert_als_text(wert("auftragsnummer"))
|
||
if not maschine_name and not auftragsnummer:
|
||
continue # leere Zeile überspringen
|
||
|
||
if not auftragsnummer:
|
||
fehlerhafte_zeilen.append(f"Zeile {zeilennummer}: FA.-Nr. fehlt.")
|
||
continue
|
||
|
||
maschine = maschinen_by_name.get(maschine_name.lower())
|
||
if not maschine:
|
||
verworfen_maschine.append(
|
||
f"Zeile {zeilennummer}: Maschine \"{maschine_name}\" nicht gefunden (FA.-Nr. {auftragsnummer})."
|
||
)
|
||
continue
|
||
|
||
try:
|
||
with db.get_cursor() as cur:
|
||
cur.execute('SELECT 1 FROM "Auftraege" WHERE auftragsnummer = %s LIMIT 1', (auftragsnummer,))
|
||
if cur.fetchone():
|
||
uebersprungen_vorhanden.append(f"Zeile {zeilennummer}: FA.-Nr. {auftragsnummer} existiert bereits.")
|
||
continue
|
||
|
||
rest = _zellwert_als_float(wert("rest_menge"), 0.0)
|
||
ist = _zellwert_als_float(wert("ist_menge"), 0.0)
|
||
soll_h = _zellwert_als_float(wert("soll_h"), 0.0)
|
||
artikel = _zellwert_als_text(wert("artikel")) or None
|
||
prio_wert = wert("prio")
|
||
|
||
einheiten = rest + ist
|
||
abgeschlossen = rest <= 0
|
||
|
||
sid_wert = None
|
||
if prio_wert not in (None, ""):
|
||
try:
|
||
sid_wert = float(prio_wert)
|
||
except (TypeError, ValueError):
|
||
sid_wert = None
|
||
if sid_wert is None:
|
||
cur.execute(
|
||
'SELECT COALESCE(MAX(sid), 0) + 1 AS next_sid FROM "Auftraege" WHERE maschine_id = %s',
|
||
(maschine["id"],),
|
||
)
|
||
sid_wert = cur.fetchone()["next_sid"]
|
||
|
||
status = "abgeschlossen" if abgeschlossen else "offen"
|
||
ts = now_ts()
|
||
cur.execute(
|
||
'INSERT INTO "Auftraege" '
|
||
'(auftragsnummer, einheiten, soll_h, status, aktiv, created_at, sid, maschine_id, artikel, start, ende) '
|
||
"VALUES (%s, %s, %s, %s, false, %s, %s, %s, %s, %s, %s)",
|
||
(
|
||
auftragsnummer, einheiten, soll_h, status, ts, sid_wert, maschine["id"], artikel,
|
||
ts if abgeschlossen else None, ts if abgeschlossen else None,
|
||
),
|
||
)
|
||
|
||
if ist > 0:
|
||
insert_mde_ohne_rate(cur, maschine["id"], auftragsnummer, ist, "start", "Import")
|
||
if abgeschlossen:
|
||
insert_mde_ohne_rate(cur, maschine["id"], auftragsnummer, einheiten, "beendet", "Import")
|
||
|
||
zusatz = " (abgeschlossen)" if abgeschlossen else ""
|
||
importiert.append(
|
||
f"Zeile {zeilennummer}: {auftragsnummer} → {maschine['name']} "
|
||
f"({fmt_num(einheiten)} Stk.{zusatz})"
|
||
)
|
||
except Exception as exc:
|
||
log.error("Import: Zeile %d fehlgeschlagen: %s", zeilennummer, exc)
|
||
fehlerhafte_zeilen.append(f"Zeile {zeilennummer}: technischer Fehler beim Speichern, wurde übersprungen.")
|
||
|
||
return render_template(
|
||
"leitstand_import.html", title="Aufträge importieren",
|
||
ergebnis={
|
||
"importiert": importiert,
|
||
"uebersprungen_vorhanden": uebersprungen_vorhanden,
|
||
"verworfen_maschine": verworfen_maschine,
|
||
"fehlerhafte_zeilen": fehlerhafte_zeilen,
|
||
},
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------
|
||
# Startseite / Healthcheck
|
||
# ---------------------------------------------------------------------
|
||
@app.route("/")
|
||
def index():
|
||
return redirect(url_for("leitstand_home"))
|
||
|
||
|
||
@app.route("/api/health")
|
||
def health():
|
||
try:
|
||
with db.get_cursor() as cur:
|
||
cur.execute("SELECT 1")
|
||
return {"status": "ok"}
|
||
except Exception as exc:
|
||
log.error("Healthcheck fehlgeschlagen: %s", exc)
|
||
return {"status": "error"}, 503
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app.run(host="0.0.0.0", port=5000, debug=False)
|