Validation_Suite/app/app.py
2026-07-10 19:13:07 +00:00

272 lines
11 KiB
Python

from __future__ import annotations
import os
from datetime import date, datetime
from pathlib import Path
from flask import Flask, flash, redirect, render_template, request, send_from_directory, url_for
from flask_login import LoginManager, UserMixin, current_user, login_required, login_user, logout_user
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func, select
from werkzeug.security import check_password_hash, generate_password_hash
from reporting.report_builder import build_validation_report
db = SQLAlchemy()
login_manager = LoginManager()
BASE_DIR = Path(__file__).resolve().parent
GENERATED_DIR = BASE_DIR / "generated"
GENERATED_DIR.mkdir(exist_ok=True)
class User(UserMixin, db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Customer(db.Model):
__tablename__ = "customers"
id = db.Column(db.Integer, primary_key=True)
practice_name = db.Column(db.String(180), nullable=False)
operator_name = db.Column(db.String(180), nullable=False)
contact_person = db.Column(db.String(180))
hygiene_contact = db.Column(db.String(180))
street = db.Column(db.String(180))
postal_code = db.Column(db.String(20))
city = db.Column(db.String(120))
email = db.Column(db.String(180))
phone = db.Column(db.String(80))
@property
def address(self):
return ", ".join(x for x in [
self.street or "",
f"{self.postal_code or ''} {self.city or ''}".strip()
] if x)
class Device(db.Model):
__tablename__ = "devices"
id = db.Column(db.Integer, primary_key=True)
customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"), nullable=False)
manufacturer = db.Column(db.String(120), nullable=False)
model = db.Column(db.String(120), nullable=False)
serial_number = db.Column(db.String(120), unique=True, nullable=False)
year_built = db.Column(db.String(20))
commissioning_date = db.Column(db.Date)
chamber_volume = db.Column(db.String(50))
steam_generation = db.Column(db.String(180))
water_supply = db.Column(db.String(180))
documentation = db.Column(db.String(180))
supplier = db.Column(db.String(180))
customer = db.relationship("Customer", backref="devices")
class Equipment(db.Model):
__tablename__ = "equipment"
id = db.Column(db.Integer, primary_key=True)
designation = db.Column(db.String(150), nullable=False)
manufacturer = db.Column(db.String(120))
model = db.Column(db.String(120))
serial_number = db.Column(db.String(120), unique=True, nullable=False)
measurement_range = db.Column(db.String(150))
calibration_date = db.Column(db.Date)
calibration_due = db.Column(db.Date)
equipment_type = db.Column(db.String(80))
class Validation(db.Model):
__tablename__ = "validations"
id = db.Column(db.Integer, primary_key=True)
report_number = db.Column(db.String(60), unique=True, nullable=False)
customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"), nullable=False)
device_id = db.Column(db.Integer, db.ForeignKey("devices.id"), nullable=False)
validation_type = db.Column(db.String(60), nullable=False)
project = db.Column(db.String(120), nullable=False)
test_date = db.Column(db.Date, nullable=False)
test_location = db.Column(db.String(120), nullable=False)
examiner = db.Column(db.String(150), nullable=False)
participants = db.Column(db.String(255))
status = db.Column(db.String(30), default="Entwurf", nullable=False)
result = db.Column(db.String(30), default="Bestanden", nullable=False)
room_temperature = db.Column(db.String(20))
room_humidity = db.Column(db.String(20))
vacuum_leak_rate = db.Column(db.String(30))
next_validation = db.Column(db.Date)
recommendations = db.Column(db.Text)
notes = db.Column(db.Text)
customer = db.relationship("Customer")
device = db.relationship("Device")
@login_manager.user_loader
def load_user(user_id):
try:
return db.session.get(User, int(user_id))
except Exception:
return None
def parse_date(value):
if not value:
return None
try:
return datetime.strptime(value, "%Y-%m-%d").date()
except ValueError:
return None
def next_report_number():
count = db.session.scalar(select(func.count()).select_from(Validation)) or 0
return f"VAL-{datetime.now().year}-{count + 1:04d}"
def seed_demo_data():
if db.session.scalar(select(func.count()).select_from(Customer)):
return
customer = Customer(
practice_name="Urologische Praxis Muster",
operator_name="Dr. Max Mustermann",
contact_person="Dr. Max Mustermann",
hygiene_contact="Frau Beispiel",
street="Musterstraße 5",
postal_code="90459",
city="Nürnberg",
email="praxis@example.de",
phone="0911 123456",
)
db.session.add(customer)
db.session.flush()
device = Device(
customer_id=customer.id,
manufacturer="Euronda",
model="E10.7",
serial_number="DEMO-EXN250688",
year_built="2025",
commissioning_date=date(2025, 12, 5),
chamber_volume="23 Liter",
steam_generation="Eigendampferzeugung",
water_supply="Automatische Versorgung über Aquafilter",
documentation="Interne CF-Card, Ausgabe am PC möglich",
supplier="Medizintechnik Muster",
)
db.session.add(device)
equipment = [
Equipment(designation="Temperatur Datenlogger 1", manufacturer="Ebro", model="EBI 11 T235", serial_number="15102807", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
Equipment(designation="Temperatur Datenlogger 2", manufacturer="Ebro", model="EBI 11 T240", serial_number="15211066", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
Equipment(designation="Temperatur Datenlogger 3", manufacturer="Ebro", model="EBI 11 T240", serial_number="15102538", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
Equipment(designation="Temperatur Datenlogger 4", manufacturer="Ebro", model="EBI 11 T240", serial_number="15211067", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
Equipment(designation="Temperatur Datenlogger 5", manufacturer="Ebro", model="EBI 11 T240", serial_number="15125738", measurement_range="0 °C bis +150 °C", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Temperatur"),
Equipment(designation="Drucklogger", manufacturer="Ebro", model="EBI 11 P111", serial_number="15092368", measurement_range="-100 mbar bis 4 bar abs.", calibration_date=date(2025,1,17), calibration_due=date(2027,1,17), equipment_type="Druck"),
]
db.session.add_all(equipment)
db.session.flush()
validation = Validation(
report_number="VAL-DEMO-0001",
customer_id=customer.id,
device_id=device.id,
validation_type="Erstvalidierung",
project="Erst-Validierung",
test_date=date(2025, 12, 5),
test_location="Nürnberg",
examiner="Andreas Schubert",
participants="Frau Beispiel, Dr. Max Mustermann",
status="Entwurf",
result="Bestanden",
room_temperature="21 °C",
room_humidity="41 %",
vacuum_leak_rate="0,2 mbar/min",
next_validation=date(2027, 11, 1),
recommendations="Auflagen und Empfehlungen werden im Kapitel 4.2 dokumentiert.",
notes="V2-Vorschau: Winlog-Messwerte, Originalbilder, Zertifikate und Logo werden später dynamisch ersetzt.",
)
db.session.add(validation)
db.session.commit()
def create_app():
app = Flask(__name__)
app.config.update(
SECRET_KEY=os.getenv("SECRET_KEY", "validation-v2-local-secret"),
SQLALCHEMY_DATABASE_URI=os.getenv("DATABASE_URL", "sqlite:///validation-v2.db"),
SQLALCHEMY_TRACK_MODIFICATIONS=False,
)
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = "login"
@app.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for("dashboard"))
if request.method == "POST":
username = (request.form.get("username") or "").strip()
password = request.form.get("password") or ""
repeat = request.form.get("password_repeat") or ""
if len(username) < 3 or len(password) < 6 or password != repeat:
flash("Bitte gültige Zugangsdaten eingeben.", "error")
elif db.session.scalar(select(User).where(func.lower(User.username) == username.lower())):
flash("Benutzername bereits vergeben.", "error")
else:
user = User(username=username)
user.set_password(password)
db.session.add(user)
db.session.commit()
return redirect(url_for("login"))
return render_template("register.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("dashboard"))
if request.method == "POST":
username = (request.form.get("username") or "").strip()
user = db.session.scalar(select(User).where(func.lower(User.username) == username.lower()))
if user and user.check_password(request.form.get("password") or ""):
login_user(user)
return redirect(url_for("dashboard"))
flash("Anmeldung fehlgeschlagen.", "error")
return render_template("login.html")
@app.route("/logout", methods=["POST"])
@login_required
def logout():
logout_user()
return redirect(url_for("login"))
@app.route("/")
@login_required
def dashboard():
validations = list(db.session.scalars(select(Validation).order_by(Validation.id.desc())))
return render_template("dashboard.html", validations=validations)
@app.route("/validations/<int:item_id>/report")
@login_required
def report(item_id):
validation = db.session.get(Validation, item_id)
if not validation:
return "Nicht gefunden", 404
equipment = list(db.session.scalars(select(Equipment).order_by(Equipment.id)))
filename = build_validation_report(validation, equipment, GENERATED_DIR)
return send_from_directory(GENERATED_DIR, filename, as_attachment=True)
with app.app_context():
db.create_all()
seed_demo_data()
return app
app = create_app()