Feature: Vollständige Züchter-Rolle — Antrag, Würfe, Stammbaum, Genetik
Basis-Features (Schritte 1–11): - Züchter-Antrag mit Dokument-Upload, Admin-Prüfung, E-Mail-Benachrichtigungen - Öffentliches Züchter-Profil + Karten-Marker (lila, certificate-Icon) - Wurfverwaltung: Würfe, Welpen, Gewichtsverlauf, Foto-System - Wurfbörse (öffentlich) mit Filtersuche nach Rasse/Status - Läufigkeits-Tracker: Deckdatum + Wurftermin (+63 Tage, nur für Züchter) - Interessenten-Chat: Kontakt-Button in Wurfbörse und Züchter-Profil - Sidebar-Einträge: Zuchtkartei + Wurfverwaltung für Züchter/Admin Stammbaum & Genetik (Schritte 1–8): - Zuchtkartei: Hunde-Stammdaten mit Vater/Mutter-Verknüpfung - Stammbaum-Visualisierung: 4 Generationen, horizontales CSS-Grid - Gesundheitstests (HD, ED, OCD, Augen…) mit farbigen Ergebnis-Badges - Genetische Tests (MDR1, PRA, DM…): clear/carrier/affected - Titel & Auszeichnungen (CAC, CACIB, IPO…) - Probeverpaarung: IK-Berechnung nach Wright + Ampel-Bewertung - Teilen-Link für öffentliche Hunde-Profile - Kaufvertrag: druckbares HTML-Dokument pro Welpe Technisch: 4 neue Route-Dateien, 5 neue Page-Module, 11 neue DB-Tabellen, icons shield-check + certificate + tree-structure im Sprite — SW by-v465, APP_VER 444
This commit is contained in:
parent
58cb2b4ad3
commit
91340be5a3
24 changed files with 6660 additions and 27 deletions
575
backend/routes/litters.py
Normal file
575
backend/routes/litters.py
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
"""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
|
||||
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
|
||||
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, 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.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,
|
||||
)
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM litters WHERE id=?", (cur.lastrowid,)
|
||||
).fetchone()
|
||||
return dict(row)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue