Add Schadenprotokoll SPA with Python Flask API
New tool for processing damage protocols: - Tab 1: Generate pre-filled PDF from Schadenlaufzettel.docx - Tab 2: Analyze filled PDF forms and extract dropdown selections - Tab 3: Generate Vorbericht.docx from filled PDF data Backend: Flask API with pypdf and python-docx Frontend: Vanilla JS SPA with drag-drop file upload 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
109
schadenprotokoll/api/app.py
Normal file
109
schadenprotokoll/api/app.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Schadenprotokoll API - Flask Backend
|
||||
Endpunkte: /generate, /analyze, /vorbericht
|
||||
"""
|
||||
from flask import Flask, request, jsonify, send_file
|
||||
from flask_cors import CORS
|
||||
import tempfile
|
||||
import os
|
||||
from processors import parse_laufzettel, fill_pdf, analyze_pdf, generate_vorbericht
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
TEMPLATE_PDF = "/opt/stacks/spa-hosting/html/schadenprotokoll/templates/protokoll.pdf"
|
||||
TEMPLATE_DOCX = "/opt/stacks/spa-hosting/html/schadenprotokoll/templates/vorbericht.docx"
|
||||
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@app.route("/generate", methods=["POST"])
|
||||
def generate():
|
||||
"""Laufzettel.docx -> vorausgefuelltes Protokoll.pdf"""
|
||||
if "file" not in request.files:
|
||||
return jsonify({"error": "Keine Datei hochgeladen"}), 400
|
||||
|
||||
file = request.files["file"]
|
||||
if not file.filename.endswith(".docx"):
|
||||
return jsonify({"error": "Nur .docx Dateien erlaubt"}), 400
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_in:
|
||||
file.save(tmp_in.name)
|
||||
|
||||
try:
|
||||
data = parse_laufzettel(tmp_in.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_out:
|
||||
fill_pdf(TEMPLATE_PDF, tmp_out.name, data)
|
||||
|
||||
os.unlink(tmp_in.name)
|
||||
return send_file(
|
||||
tmp_out.name,
|
||||
mimetype="application/pdf",
|
||||
as_attachment=True,
|
||||
download_name="Schadenprotokoll_vorbefuellt.pdf"
|
||||
)
|
||||
except Exception as e:
|
||||
os.unlink(tmp_in.name)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/analyze", methods=["POST"])
|
||||
def analyze():
|
||||
"""Ausgefuelltes PDF -> JSON mit allen Feldern"""
|
||||
if "file" not in request.files:
|
||||
return jsonify({"error": "Keine Datei hochgeladen"}), 400
|
||||
|
||||
file = request.files["file"]
|
||||
if not file.filename.endswith(".pdf"):
|
||||
return jsonify({"error": "Nur .pdf Dateien erlaubt"}), 400
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
|
||||
file.save(tmp.name)
|
||||
|
||||
try:
|
||||
result = analyze_pdf(tmp.name)
|
||||
os.unlink(tmp.name)
|
||||
return jsonify({"success": True, "data": result})
|
||||
except Exception as e:
|
||||
os.unlink(tmp.name)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/vorbericht", methods=["POST"])
|
||||
def vorbericht():
|
||||
"""Ausgefuelltes PDF -> Vorbericht.docx"""
|
||||
if "file" not in request.files:
|
||||
return jsonify({"error": "Keine Datei hochgeladen"}), 400
|
||||
|
||||
file = request.files["file"]
|
||||
if not file.filename.endswith(".pdf"):
|
||||
return jsonify({"error": "Nur .pdf Dateien erlaubt"}), 400
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_in:
|
||||
file.save(tmp_in.name)
|
||||
|
||||
try:
|
||||
pdf_data = analyze_pdf(tmp_in.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_out:
|
||||
generate_vorbericht(pdf_data, TEMPLATE_DOCX, tmp_out.name)
|
||||
|
||||
os.unlink(tmp_in.name)
|
||||
return send_file(
|
||||
tmp_out.name,
|
||||
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
as_attachment=True,
|
||||
download_name="Vorbericht.docx"
|
||||
)
|
||||
except Exception as e:
|
||||
os.unlink(tmp_in.name)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5050, debug=False)
|
||||
136
schadenprotokoll/api/processors.py
Normal file
136
schadenprotokoll/api/processors.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Schadenprotokoll Processors - PDF/DOCX Verarbeitung
|
||||
"""
|
||||
from docx import Document
|
||||
from docx.oxml.ns import qn
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def parse_laufzettel(docx_path: str) -> dict:
|
||||
"""Extrahiert Schadensdaten aus dem Laufzettel.docx"""
|
||||
data = {}
|
||||
|
||||
with zipfile.ZipFile(docx_path, "r") as z:
|
||||
xml_content = z.read("word/document.xml")
|
||||
root = ET.fromstring(xml_content)
|
||||
|
||||
ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
|
||||
|
||||
for sdt in root.findall(".//w:sdt", ns):
|
||||
alias = sdt.find(".//w:alias", ns)
|
||||
if alias is not None:
|
||||
alias_val = alias.get(qn("w:val"))
|
||||
texts = sdt.findall(".//w:t", ns)
|
||||
content = " ".join([t.text for t in texts if t.text]).strip()
|
||||
|
||||
field_map = {
|
||||
"Schadennummer": "Schadennummer",
|
||||
"Versicherungsnehmer": "Versicherungsnehmer:",
|
||||
"Versicherer": "Versicherer:",
|
||||
"Datum": "Datum:",
|
||||
"Schadenort": "Schadenort:",
|
||||
"Schadenart": "Schadenart:",
|
||||
"e-Mail": "e-Mail VN:",
|
||||
}
|
||||
|
||||
if alias_val in field_map:
|
||||
data[field_map[alias_val]] = content
|
||||
elif alias_val == "Straße":
|
||||
data["Adresse:"] = content
|
||||
elif alias_val == "Ort":
|
||||
if "Adresse:" in data:
|
||||
data["Adresse:"] += ", " + content
|
||||
else:
|
||||
data["Adresse:"] = content
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def fill_pdf(template_path: str, output_path: str, data: dict) -> str:
|
||||
"""Befuellt das PDF-Template mit den Schadensdaten"""
|
||||
reader = PdfReader(template_path)
|
||||
writer = PdfWriter()
|
||||
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
if "/Annots" in page:
|
||||
writer.update_page_form_field_values(
|
||||
writer.pages[-1], data, auto_regenerate=False
|
||||
)
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
writer.write(f)
|
||||
|
||||
return output_path
|
||||
|
||||
|
||||
def analyze_pdf(pdf_path: str) -> dict:
|
||||
"""Liest alle Formularfelder aus dem ausgefuellten PDF"""
|
||||
reader = PdfReader(pdf_path)
|
||||
result = {"textfields": {}, "dropdowns": {}, "checkboxes": {}}
|
||||
|
||||
for page_num, page in enumerate(reader.pages):
|
||||
if "/Annots" not in page:
|
||||
continue
|
||||
|
||||
for annot in page["/Annots"]:
|
||||
obj = annot.get_object()
|
||||
field_type = obj.get("/FT", "")
|
||||
field_name = obj.get("/T", f"Feld_{page_num}")
|
||||
|
||||
if field_type == "/Tx":
|
||||
value = obj.get("/V", "")
|
||||
result["textfields"][field_name] = str(value) if value else ""
|
||||
|
||||
elif field_type == "/Ch":
|
||||
value = obj.get("/V", "")
|
||||
options = []
|
||||
if "/Opt" in obj:
|
||||
for opt in obj["/Opt"]:
|
||||
if isinstance(opt, list):
|
||||
options.append(str(opt[1]) if len(opt) > 1 else str(opt[0]))
|
||||
else:
|
||||
options.append(str(opt))
|
||||
result["dropdowns"][field_name] = {
|
||||
"selected": str(value) if value else "",
|
||||
"options": options
|
||||
}
|
||||
|
||||
elif field_type == "/Btn":
|
||||
value = obj.get("/V", "/Off")
|
||||
result["checkboxes"][field_name] = value != "/Off"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_vorbericht(pdf_data: dict, template_path: str, output_path: str) -> str:
|
||||
"""Generiert Vorbericht.docx aus PDF-Daten"""
|
||||
doc = Document(template_path)
|
||||
|
||||
tf = pdf_data.get("textfields", {})
|
||||
replacements = {
|
||||
"Schadennummer": tf.get("Schadennummer", "xx"),
|
||||
"Versicherer": tf.get("Versicherer:", "xx"),
|
||||
"Versicherungsnehmer": tf.get("Versicherungsnehmer:", "xx"),
|
||||
"Adresse": tf.get("Adresse:", "xx"),
|
||||
}
|
||||
|
||||
# Dropdown-Antworten fuer Sachverhalt sammeln
|
||||
dropdowns = pdf_data.get("dropdowns", {})
|
||||
sachverhalt = []
|
||||
for key in sorted(dropdowns.keys()):
|
||||
val = dropdowns[key].get("selected", "")
|
||||
if val and val not in ["Was ist beschädigt?", "Schadenursache?", ""]:
|
||||
sachverhalt.append(val)
|
||||
|
||||
for para in doc.paragraphs:
|
||||
text = para.text
|
||||
for key, value in replacements.items():
|
||||
if key.lower() in text.lower() and "xx" in text:
|
||||
para.text = text.replace("xx", value, 1)
|
||||
break
|
||||
|
||||
doc.save(output_path)
|
||||
return output_path
|
||||
Reference in New Issue
Block a user