Tierschutz-System (immer aktiv, nicht abschaltbar): - welfare_check.py: regelbasierte Prüfung IK, Alter, Deckpause, Wurfanzahl, Genetik - Grün/Gelb/Rot-Modal bei Wurf anlegen + Probeverpaarung - Bei kritischem Befund + "Trotzdem fortfahren" → automatische Admin-Mail - Tierschutz-Check nie durch Nutzer deaktivierbar KI-Züchter-Features (pro User an/abschaltbar außer Tierschutz): - routes/zucht_ki.py: 5 Endpunkte — Wurfankündigung, Genetik-Erklärung, Paarungsanalyse, Hund-Beschreibung, Jahresbericht - Toggles in Einstellungen (ki_zucht_* Felder) - KI-Buttons in litters.js + zuchthunde.js KI-Routing: Privilegierte Rollen (Admin, Züchter, Moderator, Manager) nutzen Claude Sonnet primär, lokales LLM als Fallback Datenexport: routes/breeder_export.py — ZIP mit HTML-Dossier + ODS (odfpy hinzugefügt in requirements.txt) Admin-Profil: POST /admin/breeder/create-profile für Schnellprofil ohne Antragsprozess; Admin-Rolle bleibt erhalten Wurfformular: Dropdown aus Zuchtkartei für Vater/Mutter mit Auto-Fill; litters.vater_id + mutter_id als FK auf zucht_hunde Probeverpaarung: heart-fill Icon + Welfare-Block im Ergebnis Landing Page: Züchter-Section + Feature-Gruppe, Meta-Tags, JSON-LD, keywords, softwareVersion 2.1 SEO: llms.txt vollständig überarbeitet, robots.txt Züchter-Pfade, sitemap.xml um Wurfbörse + Züchter-Profile erweitert SW by-v474, APP_VER 451
650 lines
24 KiB
Python
650 lines
24 KiB
Python
"""BAN YARO — Wurfverwaltung (Züchter: Würfe & Welpen)"""
|
|
|
|
import logging
|
|
from datetime import date
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.responses import HTMLResponse
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
|
|
from database import db
|
|
from auth import get_current_user, get_current_user_optional
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Dependency: nur verifizierte Züchter + Admins
|
|
# ------------------------------------------------------------------
|
|
def _require_breeder(user=Depends(get_current_user)):
|
|
if user["rolle"] not in ("breeder", "admin"):
|
|
raise HTTPException(403, "Nur für verifizierte Züchter.")
|
|
return user
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Schemas
|
|
# ------------------------------------------------------------------
|
|
class LitterCreate(BaseModel):
|
|
vater_name: Optional[str] = None
|
|
mutter_name: Optional[str] = None
|
|
vater_id: Optional[int] = None # FK zucht_hunde
|
|
mutter_id: Optional[int] = None # FK zucht_hunde
|
|
geburt_datum: Optional[str] = None # YYYY-MM-DD
|
|
erwartetes_datum: Optional[str] = None # YYYY-MM-DD
|
|
welpen_gesamt: Optional[int] = None
|
|
welpen_verfuegbar: Optional[int] = None
|
|
beschreibung: Optional[str] = None
|
|
gesundheitstests: Optional[str] = None
|
|
preis_spanne: Optional[str] = None
|
|
status: str = "geplant" # geplant|geboren|verfuegbar|abgeschlossen
|
|
sichtbar: int = 0
|
|
sichtbar_bis: Optional[str] = None
|
|
|
|
|
|
class LitterUpdate(BaseModel):
|
|
vater_name: Optional[str] = None
|
|
mutter_name: Optional[str] = None
|
|
vater_id: Optional[int] = None
|
|
mutter_id: Optional[int] = None
|
|
geburt_datum: Optional[str] = None
|
|
erwartetes_datum: Optional[str] = None
|
|
welpen_gesamt: Optional[int] = None
|
|
welpen_verfuegbar: Optional[int] = None
|
|
beschreibung: Optional[str] = None
|
|
gesundheitstests: Optional[str] = None
|
|
preis_spanne: Optional[str] = None
|
|
status: Optional[str] = None
|
|
sichtbar: Optional[int] = None
|
|
sichtbar_bis: Optional[str] = None
|
|
|
|
|
|
class PuppyCreate(BaseModel):
|
|
name: Optional[str] = None
|
|
geschlecht: Optional[str] = None # maennlich|weiblich
|
|
farbe: Optional[str] = None
|
|
chip_nr: Optional[str] = None
|
|
geburtsgewicht: Optional[float] = None # Gramm
|
|
status: str = "verfuegbar" # verfuegbar|reserviert|abgegeben
|
|
status_sichtbar: int = 1
|
|
notiz: Optional[str] = None
|
|
|
|
|
|
class PuppyUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
geschlecht: Optional[str] = None
|
|
farbe: Optional[str] = None
|
|
chip_nr: Optional[str] = None
|
|
geburtsgewicht: Optional[float] = None
|
|
status: Optional[str] = None
|
|
status_sichtbar: Optional[int] = None
|
|
notiz: Optional[str] = None
|
|
|
|
|
|
class WeightEntry(BaseModel):
|
|
gewicht_g: float
|
|
gemessen_am: str # YYYY-MM-DD
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Hilfsfunktion: Züchter-Profil des Users ermitteln
|
|
# ------------------------------------------------------------------
|
|
def _get_breeder_profile(user_id: int, conn):
|
|
row = conn.execute(
|
|
"SELECT id FROM breeder_profiles WHERE user_id=?", (user_id,)
|
|
).fetchone()
|
|
return row
|
|
|
|
|
|
def _check_litter_owner(litter_id: int, user, conn):
|
|
"""Gibt den Wurf zurück wenn der User Eigentümer oder Admin ist."""
|
|
litter = conn.execute(
|
|
"SELECT l.*, bp.user_id AS owner_user_id "
|
|
"FROM litters l JOIN breeder_profiles bp ON bp.id = l.breeder_id "
|
|
"WHERE l.id=?",
|
|
(litter_id,)
|
|
).fetchone()
|
|
if not litter:
|
|
raise HTTPException(404, "Wurf nicht gefunden.")
|
|
if user["rolle"] != "admin" and litter["owner_user_id"] != user["id"]:
|
|
raise HTTPException(403, "Kein Zugriff.")
|
|
return litter
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/litters — öffentliche Übersicht
|
|
# ------------------------------------------------------------------
|
|
@router.get("/litters")
|
|
async def list_public_litters(
|
|
rasse: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
):
|
|
today = date.today().isoformat()
|
|
with db() as conn:
|
|
q = """
|
|
SELECT l.*,
|
|
bp.zwingername, bp.rasse_text, bp.stadt,
|
|
bp.user_id AS breeder_user_id,
|
|
u.name AS zuechter_name
|
|
FROM litters l
|
|
JOIN breeder_profiles bp ON bp.id = l.breeder_id
|
|
JOIN users u ON u.id = bp.user_id
|
|
WHERE l.sichtbar = 1
|
|
AND (l.sichtbar_bis IS NULL OR l.sichtbar_bis >= ?)
|
|
"""
|
|
params = [today]
|
|
|
|
if status:
|
|
q += " AND l.status = ?"
|
|
params.append(status)
|
|
else:
|
|
q += " AND l.status IN ('geplant', 'geboren', 'verfuegbar')"
|
|
|
|
if rasse:
|
|
q += " AND LOWER(bp.rasse_text) LIKE LOWER(?)"
|
|
params.append(f"%{rasse}%")
|
|
|
|
q += " ORDER BY l.created_at DESC"
|
|
rows = conn.execute(q, params).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/litters/my — eigene Würfe (Züchter)
|
|
# ------------------------------------------------------------------
|
|
@router.get("/litters/my")
|
|
async def my_litters(user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
if user["rolle"] == "admin":
|
|
# Admin ohne eigenes Profil sieht alle Würfe aller Züchter
|
|
profile = _get_breeder_profile(user["id"], conn)
|
|
if not profile:
|
|
rows = conn.execute(
|
|
"SELECT l.*, bp.zwingername FROM litters l "
|
|
"JOIN breeder_profiles bp ON bp.id = l.breeder_id "
|
|
"ORDER BY l.created_at DESC"
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
else:
|
|
profile = _get_breeder_profile(user["id"], conn)
|
|
if not profile:
|
|
raise HTTPException(404, "Kein Züchter-Profil vorhanden.")
|
|
rows = conn.execute(
|
|
"SELECT * FROM litters WHERE breeder_id=? ORDER BY created_at DESC",
|
|
(profile["id"],)
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/litters — neuen Wurf anlegen
|
|
# ------------------------------------------------------------------
|
|
@router.post("/litters", status_code=201)
|
|
async def create_litter(body: LitterCreate, user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
profile = _get_breeder_profile(user["id"], conn)
|
|
if not profile:
|
|
raise HTTPException(404, "Züchter-Profil nicht gefunden.")
|
|
|
|
cur = conn.execute(
|
|
"""INSERT INTO litters
|
|
(breeder_id, vater_name, mutter_name, vater_id, mutter_id,
|
|
geburt_datum, erwartetes_datum,
|
|
welpen_gesamt, welpen_verfuegbar, beschreibung, gesundheitstests,
|
|
preis_spanne, status, sichtbar, sichtbar_bis)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
profile["id"],
|
|
body.vater_name,
|
|
body.mutter_name,
|
|
body.vater_id,
|
|
body.mutter_id,
|
|
body.geburt_datum,
|
|
body.erwartetes_datum,
|
|
body.welpen_gesamt,
|
|
body.welpen_verfuegbar,
|
|
body.beschreibung,
|
|
body.gesundheitstests,
|
|
body.preis_spanne,
|
|
body.status,
|
|
body.sichtbar,
|
|
body.sichtbar_bis,
|
|
)
|
|
)
|
|
litter_id = cur.lastrowid
|
|
row = conn.execute(
|
|
"SELECT * FROM litters WHERE id=?", (litter_id,)
|
|
).fetchone()
|
|
|
|
# Tierschutz-Check
|
|
from welfare_check import check_welfare
|
|
welfare = check_welfare(
|
|
conn, profile["id"],
|
|
vater_id=body.vater_id,
|
|
mutter_id=body.mutter_id,
|
|
)
|
|
# Welfare-Level speichern
|
|
conn.execute(
|
|
"UPDATE litters SET welfare_level=? WHERE id=?",
|
|
(welfare["level"], litter_id)
|
|
)
|
|
|
|
result = dict(row)
|
|
result["welfare"] = welfare
|
|
return result
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/litters/{id}/welfare-confirm — Tierschutz-Hinweis bestätigt
|
|
# ------------------------------------------------------------------
|
|
@router.post("/litters/{litter_id}/welfare-confirm")
|
|
async def welfare_confirm(litter_id: int, user=Depends(_require_breeder)):
|
|
from mailer import send_email
|
|
import os, logging as _log
|
|
_logger = _log.getLogger(__name__)
|
|
|
|
with db() as conn:
|
|
litter = _check_litter_owner(litter_id, user, conn)
|
|
conn.execute(
|
|
"UPDATE litters SET welfare_acknowledged=1 WHERE id=?", (litter_id,)
|
|
)
|
|
welfare_level = litter.get("welfare_level", "")
|
|
|
|
if welfare_level == "critical":
|
|
# Admin benachrichtigen
|
|
profile = conn.execute(
|
|
"SELECT bp.zwingername, u.name, u.email "
|
|
"FROM breeder_profiles bp JOIN users u ON u.id=bp.user_id "
|
|
"WHERE bp.user_id=?", (user["id"],)
|
|
).fetchone()
|
|
admin_email = os.getenv("ADMIN_EMAIL", "mail@motocamp.de")
|
|
app_url = os.getenv("APP_URL", "https://banyaro.app")
|
|
zuechter = profile["name"] if profile else user.get("name", "Unbekannt")
|
|
zwinger = profile["zwingername"] if profile else "—"
|
|
eltern = conn.execute(
|
|
"SELECT vater_name, mutter_name FROM litters WHERE id=?", (litter_id,)
|
|
).fetchone()
|
|
html = f"""
|
|
<h2>Tierschutz-Hinweis bestätigt</h2>
|
|
<p>Züchter <b>{zuechter}</b> (Zwinger: {zwinger}) hat einen Wurf mit
|
|
kritischen Tierschutz-Hinweisen trotzdem angelegt.</p>
|
|
<p>Vater: {eltern['vater_name'] or '—'} · Mutter: {eltern['mutter_name'] or '—'}</p>
|
|
<p>Wurf-ID: {litter_id}</p>
|
|
<p><a href="{app_url}/admin">Im Admin-Bereich prüfen</a></p>
|
|
"""
|
|
try:
|
|
await send_email(
|
|
admin_email,
|
|
f"[Banyaro Tierschutz] Kritischer Hinweis bestätigt — {zwinger}",
|
|
html,
|
|
f"Züchter {zuechter} hat Wurf #{litter_id} trotz kritischer Tierschutz-Hinweise angelegt.",
|
|
)
|
|
except Exception as e:
|
|
_logger.warning(f"Tierschutz-Admin-Mail fehlgeschlagen: {e}")
|
|
|
|
return {"message": "Bestätigt."}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/litters/{id} — Wurf-Detail (öffentlich wenn sichtbar=1)
|
|
# ------------------------------------------------------------------
|
|
@router.get("/litters/{litter_id}")
|
|
async def get_litter(litter_id: int, user=Depends(get_current_user_optional)):
|
|
today = date.today().isoformat()
|
|
with db() as conn:
|
|
row = conn.execute(
|
|
"""SELECT l.*,
|
|
bp.zwingername, bp.rasse_text, bp.stadt,
|
|
bp.user_id AS owner_user_id,
|
|
u.name AS zuechter_name
|
|
FROM litters l
|
|
JOIN breeder_profiles bp ON bp.id = l.breeder_id
|
|
JOIN users u ON u.id = bp.user_id
|
|
WHERE l.id=?""",
|
|
(litter_id,)
|
|
).fetchone()
|
|
|
|
if not row:
|
|
raise HTTPException(404, "Wurf nicht gefunden.")
|
|
|
|
is_owner = user and (
|
|
user["rolle"] == "admin" or row["owner_user_id"] == user["id"]
|
|
)
|
|
|
|
# Nicht-öffentliche Würfe nur für Züchter/Admin
|
|
if not row["sichtbar"] and not is_owner:
|
|
raise HTTPException(404, "Wurf nicht gefunden.")
|
|
|
|
# Abgelaufene Würfe
|
|
if row["sichtbar_bis"] and row["sichtbar_bis"] < today and not is_owner:
|
|
raise HTTPException(404, "Wurf nicht mehr verfügbar.")
|
|
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# PUT /api/litters/{id} — Wurf bearbeiten
|
|
# ------------------------------------------------------------------
|
|
@router.put("/litters/{litter_id}")
|
|
async def update_litter(litter_id: int, body: LitterUpdate, user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
_check_litter_owner(litter_id, user, conn)
|
|
|
|
fields, params = [], []
|
|
for field, value in body.model_dump(exclude_none=True).items():
|
|
fields.append(f"{field}=?")
|
|
params.append(value)
|
|
|
|
if not fields:
|
|
raise HTTPException(400, "Keine Felder zum Aktualisieren.")
|
|
|
|
params.append(litter_id)
|
|
conn.execute(
|
|
f"UPDATE litters SET {', '.join(fields)} WHERE id=?", params
|
|
)
|
|
row = conn.execute("SELECT * FROM litters WHERE id=?", (litter_id,)).fetchone()
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# DELETE /api/litters/{id} — Wurf löschen
|
|
# ------------------------------------------------------------------
|
|
@router.delete("/litters/{litter_id}", status_code=204)
|
|
async def delete_litter(litter_id: int, user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
_check_litter_owner(litter_id, user, conn)
|
|
conn.execute("DELETE FROM puppy_weights WHERE welpe_id IN (SELECT id FROM puppies WHERE wurf_id=?)", (litter_id,))
|
|
conn.execute("DELETE FROM puppies WHERE wurf_id=?", (litter_id,))
|
|
conn.execute("DELETE FROM litters WHERE id=?", (litter_id,))
|
|
return None
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/litters/{id}/puppies — Welpen eines Wurfs
|
|
# ------------------------------------------------------------------
|
|
@router.get("/litters/{litter_id}/puppies")
|
|
async def list_puppies(litter_id: int, user=Depends(get_current_user_optional)):
|
|
with db() as conn:
|
|
litter = conn.execute(
|
|
"""SELECT l.sichtbar, l.sichtbar_bis, bp.user_id AS owner_user_id
|
|
FROM litters l JOIN breeder_profiles bp ON bp.id = l.breeder_id
|
|
WHERE l.id=?""",
|
|
(litter_id,)
|
|
).fetchone()
|
|
if not litter:
|
|
raise HTTPException(404, "Wurf nicht gefunden.")
|
|
|
|
is_owner = user and (
|
|
user["rolle"] == "admin" or litter["owner_user_id"] == user["id"]
|
|
)
|
|
|
|
q = "SELECT * FROM puppies WHERE wurf_id=?"
|
|
params = [litter_id]
|
|
if not is_owner:
|
|
q += " AND status_sichtbar=1"
|
|
|
|
rows = conn.execute(q + " ORDER BY created_at ASC", params).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/litters/{id}/puppies — Welpe anlegen
|
|
# ------------------------------------------------------------------
|
|
@router.post("/litters/{litter_id}/puppies", status_code=201)
|
|
async def add_puppy(litter_id: int, body: PuppyCreate, user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
_check_litter_owner(litter_id, user, conn)
|
|
|
|
cur = conn.execute(
|
|
"""INSERT INTO puppies
|
|
(wurf_id, name, geschlecht, farbe, chip_nr, geburtsgewicht,
|
|
status, status_sichtbar, notiz)
|
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
litter_id,
|
|
body.name,
|
|
body.geschlecht,
|
|
body.farbe,
|
|
body.chip_nr,
|
|
body.geburtsgewicht,
|
|
body.status,
|
|
body.status_sichtbar,
|
|
body.notiz,
|
|
)
|
|
)
|
|
row = conn.execute(
|
|
"SELECT * FROM puppies WHERE id=?", (cur.lastrowid,)
|
|
).fetchone()
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# PUT /api/litters/puppies/{id} — Welpe bearbeiten
|
|
# ------------------------------------------------------------------
|
|
@router.put("/litters/puppies/{puppy_id}")
|
|
async def update_puppy(puppy_id: int, body: PuppyUpdate, user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
puppy = conn.execute(
|
|
"""SELECT p.*, l.id AS litter_id, bp.user_id AS owner_user_id
|
|
FROM puppies p
|
|
JOIN litters l ON l.id = p.wurf_id
|
|
JOIN breeder_profiles bp ON bp.id = l.breeder_id
|
|
WHERE p.id=?""",
|
|
(puppy_id,)
|
|
).fetchone()
|
|
if not puppy:
|
|
raise HTTPException(404, "Welpe nicht gefunden.")
|
|
if user["rolle"] != "admin" and puppy["owner_user_id"] != user["id"]:
|
|
raise HTTPException(403, "Kein Zugriff.")
|
|
|
|
fields, params = [], []
|
|
for field, value in body.model_dump(exclude_none=True).items():
|
|
fields.append(f"{field}=?")
|
|
params.append(value)
|
|
|
|
if not fields:
|
|
raise HTTPException(400, "Keine Felder zum Aktualisieren.")
|
|
|
|
params.append(puppy_id)
|
|
conn.execute(
|
|
f"UPDATE puppies SET {', '.join(fields)} WHERE id=?", params
|
|
)
|
|
row = conn.execute("SELECT * FROM puppies WHERE id=?", (puppy_id,)).fetchone()
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/litters/puppies/{id}/weights — Gewichtsverlauf laden
|
|
# ------------------------------------------------------------------
|
|
@router.get("/litters/puppies/{puppy_id}/weights")
|
|
async def get_weights(puppy_id: int, user=Depends(get_current_user_optional)):
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
"SELECT id, gewicht_g, gemessen_am FROM puppy_weights WHERE welpe_id=? ORDER BY gemessen_am DESC",
|
|
(puppy_id,)
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/litters/puppies/{id}/weight — Gewicht erfassen
|
|
# ------------------------------------------------------------------
|
|
@router.post("/litters/puppies/{puppy_id}/weight", status_code=201)
|
|
async def add_weight(puppy_id: int, body: WeightEntry, user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
puppy = conn.execute(
|
|
"""SELECT p.id, bp.user_id AS owner_user_id
|
|
FROM puppies p
|
|
JOIN litters l ON l.id = p.wurf_id
|
|
JOIN breeder_profiles bp ON bp.id = l.breeder_id
|
|
WHERE p.id=?""",
|
|
(puppy_id,)
|
|
).fetchone()
|
|
if not puppy:
|
|
raise HTTPException(404, "Welpe nicht gefunden.")
|
|
if user["rolle"] != "admin" and puppy["owner_user_id"] != user["id"]:
|
|
raise HTTPException(403, "Kein Zugriff.")
|
|
|
|
cur = conn.execute(
|
|
"INSERT INTO puppy_weights (welpe_id, gewicht_g, gemessen_am) VALUES (?,?,?)",
|
|
(puppy_id, body.gewicht_g, body.gemessen_am)
|
|
)
|
|
row = conn.execute(
|
|
"SELECT * FROM puppy_weights WHERE id=?", (cur.lastrowid,)
|
|
).fetchone()
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/litters/puppies/{id}/contract — Kaufvertrag als HTML
|
|
# ------------------------------------------------------------------
|
|
@router.get("/litters/puppies/{puppy_id}/contract")
|
|
async def generate_contract(
|
|
puppy_id: int,
|
|
kaeufer_name: str,
|
|
kaeufer_adresse: str,
|
|
kaeufer_email: str = "",
|
|
preis: str = "",
|
|
user=Depends(_require_breeder),
|
|
):
|
|
with db() as conn:
|
|
puppy = conn.execute(
|
|
"""SELECT p.*, l.geburt_datum, l.id AS litter_id,
|
|
bp.user_id AS owner_user_id,
|
|
bp.zwingername, bp.rasse_text, bp.stadt,
|
|
u.name AS zuechter_name, u.email AS zuechter_email
|
|
FROM puppies p
|
|
JOIN litters l ON l.id = p.wurf_id
|
|
JOIN breeder_profiles bp ON bp.id = l.breeder_id
|
|
JOIN users u ON u.id = bp.user_id
|
|
WHERE p.id=?""",
|
|
(puppy_id,)
|
|
).fetchone()
|
|
|
|
if not puppy:
|
|
raise HTTPException(404, "Welpe nicht gefunden.")
|
|
if user["rolle"] != "admin" and puppy["owner_user_id"] != user["id"]:
|
|
raise HTTPException(403, "Kein Zugriff.")
|
|
|
|
def esc(s):
|
|
if not s:
|
|
return ""
|
|
return (str(s)
|
|
.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace('"', """))
|
|
|
|
heute = date.today().strftime("%d.%m.%Y")
|
|
|
|
geschlecht_label = (
|
|
"Rüde" if puppy["geschlecht"] == "maennlich" else
|
|
"Hündin" if puppy["geschlecht"] == "weiblich" else "—"
|
|
)
|
|
|
|
geburtsdatum = ""
|
|
if puppy["geburt_datum"]:
|
|
try:
|
|
from datetime import date as _date
|
|
gd = _date.fromisoformat(puppy["geburt_datum"])
|
|
geburtsdatum = gd.strftime("%d.%m.%Y")
|
|
except Exception:
|
|
geburtsdatum = esc(puppy["geburt_datum"])
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Kaufvertrag — {esc(puppy['name'] or 'Welpe')}</title>
|
|
<style>
|
|
body {{
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
font-size: 12pt;
|
|
margin: 2cm 2.5cm;
|
|
color: #111;
|
|
}}
|
|
h1 {{ font-size: 18pt; text-align: center; margin-bottom: 0.2cm; }}
|
|
h2 {{ font-size: 13pt; margin-top: 1.2cm; border-bottom: 1px solid #999; padding-bottom: 3px; }}
|
|
table {{ width: 100%; border-collapse: collapse; margin-top: 0.4cm; }}
|
|
td {{ padding: 4px 8px; vertical-align: top; }}
|
|
td:first-child {{ width: 45%; font-weight: bold; color: #444; }}
|
|
.section {{ margin-top: 1cm; }}
|
|
.signature-block {{ display: flex; gap: 4cm; margin-top: 2cm; }}
|
|
.signature-line {{ flex: 1; }}
|
|
.signature-line hr {{ border: none; border-top: 1px solid #333; margin-top: 2cm; }}
|
|
.signature-line p {{ font-size: 10pt; color: #555; margin: 4px 0 0; }}
|
|
@media print {{
|
|
.no-print {{ display: none; }}
|
|
body {{ margin: 1.5cm 2cm; }}
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<p style="text-align:right;font-size:10pt;color:#666">Datum: {heute}</p>
|
|
<h1>Kaufvertrag über einen Welpen</h1>
|
|
<p style="text-align:center;color:#555;font-size:10pt;margin-top:0">
|
|
Rassehund · {esc(puppy['rasse_text'] or '')}
|
|
</p>
|
|
|
|
<h2>Verkäufer (Züchter)</h2>
|
|
<table>
|
|
<tr><td>Zwingername</td><td>{esc(puppy['zwingername'] or '—')}</td></tr>
|
|
<tr><td>Name</td><td>{esc(puppy['zuechter_name'] or '—')}</td></tr>
|
|
<tr><td>Ort</td><td>{esc(puppy['stadt'] or '—')}</td></tr>
|
|
<tr><td>E-Mail</td><td>{esc(puppy['zuechter_email'] or '—')}</td></tr>
|
|
</table>
|
|
|
|
<h2>Käufer</h2>
|
|
<table>
|
|
<tr><td>Name</td><td>{esc(kaeufer_name)}</td></tr>
|
|
<tr><td>Adresse</td><td>{esc(kaeufer_adresse)}</td></tr>
|
|
<tr><td>E-Mail</td><td>{esc(kaeufer_email) if kaeufer_email else '—'}</td></tr>
|
|
</table>
|
|
|
|
<h2>Welpe</h2>
|
|
<table>
|
|
<tr><td>Name</td><td>{esc(puppy['name'] or '—')}</td></tr>
|
|
<tr><td>Geschlecht</td><td>{geschlecht_label}</td></tr>
|
|
<tr><td>Rasse</td><td>{esc(puppy['rasse_text'] or '—')}</td></tr>
|
|
<tr><td>Geburtsdatum</td><td>{geburtsdatum or '—'}</td></tr>
|
|
<tr><td>Chip-Nr.</td><td>{esc(puppy['chip_nr'] or '—')}</td></tr>
|
|
<tr><td>Farbe / Fell</td><td>{esc(puppy['farbe'] or '—')}</td></tr>
|
|
</table>
|
|
|
|
<h2>Kaufpreis</h2>
|
|
<table>
|
|
<tr><td>Vereinbarter Preis</td><td><strong>{esc(preis) if preis else '—'}</strong></td></tr>
|
|
</table>
|
|
|
|
<div class="section">
|
|
<h2>Allgemeine Vereinbarungen</h2>
|
|
<p>Der Käufer bestätigt, den Welpen in einem einwandfreien Gesundheitszustand entgegengenommen zu haben.
|
|
Der Verkäufer sichert zu, dass der Welpe nach bestem Wissen und Gewissen aufgezogen wurde und die
|
|
angegebenen Gesundheitsinformationen der Wahrheit entsprechen. Der Käufer verpflichtet sich, den
|
|
Welpen artgerecht zu halten und tierärztlich versorgen zu lassen.</p>
|
|
</div>
|
|
|
|
<div class="signature-block">
|
|
<div class="signature-line">
|
|
<hr>
|
|
<p>Ort, Datum & Unterschrift Verkäufer</p>
|
|
</div>
|
|
<div class="signature-line">
|
|
<hr>
|
|
<p>Ort, Datum & Unterschrift Käufer</p>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="no-print" style="margin-top:1.5cm;text-align:center">
|
|
<button onclick="window.print()"
|
|
style="padding:8px 24px;font-size:12pt;cursor:pointer;border:1px solid #333;border-radius:4px;background:#f5f5f5">
|
|
Drucken / Als PDF speichern
|
|
</button>
|
|
</p>
|
|
|
|
</body>
|
|
</html>"""
|
|
|
|
return HTMLResponse(content=html)
|