"""BAN YARO — Züchter-Datenexport (HTML + ODS)""" import io import zipfile import logging from datetime import date, datetime from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from database import db from auth import get_current_user router = APIRouter() logger = logging.getLogger(__name__) def _require_breeder(user=Depends(get_current_user)): if user["rolle"] not in ("breeder", "admin"): raise HTTPException(403, "Nur für Züchter.") return user # ------------------------------------------------------------------ # Datenabruf # ------------------------------------------------------------------ def _collect_data(user_id: int) -> dict: with db() as conn: profile = conn.execute( "SELECT bp.*, u.name AS user_name, u.email " "FROM breeder_profiles bp JOIN users u ON u.id = bp.user_id " "WHERE bp.user_id=?", (user_id,) ).fetchone() if not profile: return None bid = profile["id"] hunde = conn.execute( "SELECT * FROM zucht_hunde WHERE breeder_id=? ORDER BY name", (bid,) ).fetchall() hund_ids = [h["id"] for h in hunde] def rows_for(table, id_col): if not hund_ids: return [] placeholders = ",".join("?" * len(hund_ids)) return conn.execute( f"SELECT * FROM {table} WHERE {id_col} IN ({placeholders}) ORDER BY {id_col}", hund_ids ).fetchall() health = rows_for("dog_health_tests", "hund_id") genetic = rows_for("dog_genetic_tests", "hund_id") titles = rows_for("dog_titles", "hund_id") litters = conn.execute( "SELECT * FROM litters WHERE breeder_id=? ORDER BY created_at DESC", (bid,) ).fetchall() litter_ids = [l["id"] for l in litters] puppies, weights = [], [] if litter_ids: placeholders = ",".join("?" * len(litter_ids)) puppies = conn.execute( f"SELECT * FROM puppies WHERE wurf_id IN ({placeholders}) ORDER BY wurf_id", litter_ids ).fetchall() puppy_ids = [p["id"] for p in puppies] if puppy_ids: ph2 = ",".join("?" * len(puppy_ids)) weights = conn.execute( f"SELECT * FROM puppy_weights WHERE welpe_id IN ({ph2}) ORDER BY welpe_id, gemessen_am", puppy_ids ).fetchall() # Stammbaum pro Hund (3 Generationen, flach) def pedigree_flat(hund_id, depth=3): if not hund_id or depth == 0: return {} row = conn.execute("SELECT id, name, zuchtbuchnummer, geburtsdatum FROM zucht_hunde WHERE id=?", (hund_id,)).fetchone() if not row: return {} result = {"name": row["name"], "nr": row["zuchtbuchnummer"] or "", "geb": row["geburtsdatum"] or ""} row2 = conn.execute("SELECT vater_id, mutter_id FROM zucht_hunde WHERE id=?", (hund_id,)).fetchone() if row2: result["vater"] = pedigree_flat(row2["vater_id"], depth - 1) result["mutter"] = pedigree_flat(row2["mutter_id"], depth - 1) return result pedigrees = {h["id"]: pedigree_flat(h["id"]) for h in hunde} return { "profile": dict(profile), "hunde": [dict(h) for h in hunde], "health": [dict(r) for r in health], "genetic": [dict(r) for r in genetic], "titles": [dict(r) for r in titles], "litters": [dict(l) for l in litters], "puppies": [dict(p) for p in puppies], "weights": [dict(w) for w in weights], "pedigrees": pedigrees, } # ------------------------------------------------------------------ # HTML-Export # ------------------------------------------------------------------ def _fmt_date(val): if not val: return "—" try: return datetime.strptime(str(val)[:10], "%Y-%m-%d").strftime("%d.%m.%Y") except Exception: return str(val) def _health_badge(test_typ, ergebnis): color = "#6B7280" e = (ergebnis or "").upper() if test_typ == "HD": color = {"A1": "#16a34a", "A2": "#16a34a", "B1": "#86efac", "B2": "#86efac", "C": "#eab308", "C1": "#eab308", "C2": "#eab308", "D": "#f97316", "D1": "#f97316", "D2": "#f97316", "E": "#dc2626", "E1": "#dc2626", "E2": "#dc2626"}.get(e, "#6B7280") elif test_typ == "ED": color = {"0": "#16a34a", "1": "#eab308", "2": "#f97316", "3": "#dc2626"}.get(e, "#6B7280") elif ergebnis in ("clear", "frei"): color = "#16a34a" elif ergebnis == "carrier": color = "#eab308" elif ergebnis == "affected": color = "#dc2626" return f'{ergebnis}' def _pedigree_node_html(node, gen): if not node: return '
| Geschlecht | {geschlecht} | Geburtsdatum | {_fmt_date(h.get('geburtsdatum'))} |
|---|---|---|---|
| Chip-Nr. | {h.get('chip_nr') or '—'} | Zuchtbuchnr. | {h.get('zuchtbuchnummer') or '—'} |
| Farbe | {h.get('farbe') or '—'} | Tätowierung | {h.get('taetowiernummer') or '—'} |
| Züchter | {h.get('zuechter_name') or '—'} | Eigentümer | {h.get('eigentuemer_name') or '—'} |
Keine Elterntiere eingetragen.
'}| Test | Ergebnis | Datum | Labor | Untersucher |
|---|
| Marker | Ergebnis | Datum | Labor |
|---|
| Titel | Typ | Datum | Ort | Richter |
|---|
{l["beschreibung"]}
' if l.get("beschreibung") else ''}| Name | Geschlecht | Farbe | Chip-Nr. | Status |
|---|
| Rasse | {p.get('rasse_text','—')} | Verein | {p.get('verein','—')} |
|---|---|---|---|
| Stadt | {p.get('stadt','—')} | VDH | {'Ja' if p.get('vdh_mitglied') else 'Nein'} |
| Website | {p["website"]} | ||
{p["beschreibung"]}
' if p.get('beschreibung') else ''}Keine Hunde eingetragen.
'}Keine Würfe eingetragen.
'} """ # ------------------------------------------------------------------ # ODS-Export # ------------------------------------------------------------------ def _generate_ods(data: dict) -> bytes: from odf.opendocument import OpenDocumentSpreadsheet from odf.style import Style, TextProperties, TableColumnProperties from odf.text import P from odf.table import Table, TableColumn, TableRow, TableCell doc = OpenDocumentSpreadsheet() def add_style(name, bold=False, bg=None, color=None): style = Style(name=name, family="table-cell") tp = TextProperties() if bold: tp.setAttribute("fo:font-weight", "bold") if color: tp.setAttribute("fo:color", color) style.addElement(tp) doc.styles.addElement(style) return style header_style = add_style("Header", bold=True, color="#ffffff") normal_style = add_style("Normal") def cell(value, style=None): tc = TableCell() if style: tc.setAttribute("table:style-name", style.getAttribute("style:name")) tc.addElement(P(text=str(value) if value is not None else "")) return tc def header_row(sheet, cols): tr = TableRow() for c in cols: tr.addElement(cell(c, header_style)) sheet.addElement(tr) def data_row(sheet, vals): tr = TableRow() for v in vals: tr.addElement(cell(v)) sheet.addElement(tr) # Sheet: Hunde sheet_hunde = Table(name="Hunde") header_row(sheet_hunde, ["Name", "Rufname", "Geschlecht", "Geburtsdatum", "Chip-Nr.", "Zuchtbuchnummer", "Farbe", "Tätowierung", "Züchter", "Eigentümer", "Notiz"]) for h in data["hunde"]: g = {"maennlich": "Rüde", "weiblich": "Hündin"}.get(h.get("geschlecht",""), "") data_row(sheet_hunde, [h["name"], h.get("rufname",""), g, h.get("geburtsdatum",""), h.get("chip_nr",""), h.get("zuchtbuchnummer",""), h.get("farbe",""), h.get("taetowiernummer",""), h.get("zuechter_name",""), h.get("eigentuemer_name",""), h.get("notiz","")]) doc.spreadsheet.addElement(sheet_hunde) # Sheet: Gesundheitstests sheet_health = Table(name="Gesundheitstests") header_row(sheet_health, ["Hund", "Test-Typ", "Test-Name", "Ergebnis", "Datum", "Gültig bis", "Untersucher", "Labor", "Zertifikat-Nr."]) hund_map = {h["id"]: h["name"] for h in data["hunde"]} for r in data["health"]: data_row(sheet_health, [hund_map.get(r["hund_id"],""), r["test_typ"], r.get("test_name",""), r["ergebnis"], r.get("untersuch_am",""), r.get("gueltig_bis",""), r.get("untersucher",""), r.get("labor",""), r.get("zertifikat_nr","")]) doc.spreadsheet.addElement(sheet_health) # Sheet: Gentests sheet_gen = Table(name="Gentests") header_row(sheet_gen, ["Hund", "Marker", "Kategorie", "Genotyp", "Ergebnis", "Getestet am", "Labor", "Zertifikat-Nr."]) for r in data["genetic"]: data_row(sheet_gen, [hund_map.get(r["hund_id"],""), r["marker_name"], r.get("marker_kategorie",""), r.get("genotyp",""), r.get("ergebnis_klasse",""), r.get("getestet_am",""), r.get("labor",""), r.get("zertifikat_nr","")]) doc.spreadsheet.addElement(sheet_gen) # Sheet: Titel sheet_titles = Table(name="Titel") header_row(sheet_titles, ["Hund", "Titel", "Typ", "Verliehen am", "Ort", "Ausstellung", "Richter", "Formwert"]) for r in data["titles"]: data_row(sheet_titles, [hund_map.get(r["hund_id"],""), r["titel_name"], r["titel_typ"], r.get("verliehen_am",""), r.get("ort",""), r.get("ausstellung",""), r.get("richter",""), r.get("formwert","")]) doc.spreadsheet.addElement(sheet_titles) # Sheet: Würfe sheet_litters = Table(name="Würfe") header_row(sheet_litters, ["Vater", "Mutter", "Geburtsdatum", "Erwartetes Datum", "Status", "Welpen gesamt", "Welpen verfügbar", "Preisspanne", "Gesundheitstests", "Beschreibung"]) for l in data["litters"]: status = {"geplant":"Geplant","geboren":"Geboren","verfuegbar":"Verfügbar","abgeschlossen":"Abgeschlossen"}.get(l.get("status",""), l.get("status","")) data_row(sheet_litters, [l.get("vater_name",""), l.get("mutter_name",""), l.get("geburt_datum",""), l.get("erwartetes_datum",""), status, l.get("welpen_gesamt",""), l.get("welpen_verfuegbar",""), l.get("preis_spanne",""), l.get("gesundheitstests",""), l.get("beschreibung","")]) doc.spreadsheet.addElement(sheet_litters) # Sheet: Welpen sheet_puppies = Table(name="Welpen") header_row(sheet_puppies, ["Wurf (Vater × Mutter)", "Name", "Geschlecht", "Farbe", "Chip-Nr.", "Geburtsgewicht (g)", "Status", "Notiz"]) litter_map = {l["id"]: f"{l.get('vater_name','?')} × {l.get('mutter_name','?')}" for l in data["litters"]} for p in data["puppies"]: g = {"maennlich":"Rüde","weiblich":"Hündin"}.get(p.get("geschlecht",""),"") data_row(sheet_puppies, [litter_map.get(p["wurf_id"],""), p.get("name",""), g, p.get("farbe",""), p.get("chip_nr",""), p.get("geburtsgewicht",""), p.get("status",""), p.get("notiz","")]) doc.spreadsheet.addElement(sheet_puppies) # Sheet: Gewichte sheet_weights = Table(name="Gewichtsverlauf") header_row(sheet_weights, ["Welpe", "Gewicht (g)", "Gemessen am"]) puppy_map = {p["id"]: p.get("name","") or f"Welpe #{p['id']}" for p in data["puppies"]} for w in data["weights"]: data_row(sheet_weights, [puppy_map.get(w["welpe_id"],""), w["gewicht_g"], w.get("gemessen_am","")]) doc.spreadsheet.addElement(sheet_weights) buf = io.BytesIO() doc.save(buf) return buf.getvalue() # ------------------------------------------------------------------ # GET /api/breeder/export — ZIP mit HTML + ODS # ------------------------------------------------------------------ @router.get("/breeder/export") async def export_breeder_data(user=Depends(_require_breeder)): data = _collect_data(user["id"]) if not data: raise HTTPException(404, "Kein Züchter-Profil vorhanden.") zwinger = data["profile"].get("zwingername", "export").replace(" ", "_") today = date.today().strftime("%Y-%m-%d") name = f"banyaro_{zwinger}_{today}" try: html_bytes = _generate_html(data).encode("utf-8") ods_bytes = _generate_ods(data) except Exception as e: logger.error(f"Export-Fehler: {e}", exc_info=True) raise HTTPException(500, "Fehler beim Erstellen des Exports.") zip_buf = io.BytesIO() with zipfile.ZipFile(zip_buf, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr(f"{name}.html", html_bytes) zf.writestr(f"{name}.ods", ods_bytes) zip_buf.seek(0) return StreamingResponse( zip_buf, media_type="application/zip", headers={"Content-Disposition": f'attachment; filename="{name}.zip"'}, )