Amt-Blatt raus, Bedienung ueber die API, Erststart fragt nach zwei Wegen

Das zweite Blatt "fuer das Amt" trug DIESELBE Rechnungsnummer, aber einen
anderen Gesamtbetrag. Mehrere Blaetter zum selben Umsatz mit verschiedenen
Endbetraegen sehen aus, als werde ueber verschiedene Umsaetze abgerechnet -
bei einer Pruefung ein Problem. Ersatzlos entfallen; was die Stadt braucht,
rechnet das Steuerjournal aus den Kenndaten der PDF. Mit dem Blatt fielen
_AmtMarke, gesamtrechnung_betont und der doppelte Renderlauf weg.

Die API kann das Fenster jetzt BEDIENEN - mit Schluessel wie bei einem SSH-Key:
32 Byte Zufall in api_schluessel.txt im gemeinsamen Ordner, den nur das
Steuerjournal und der Betreiber lesen. Ohne gueltigen Schluessel kein Zutritt
(compare_digest). ping und schau_nach bleiben schluessellos, sonst kann ein
aelteres Journal nicht mehr anklopfen. Was einen BELEG schreibt, geht nur im
Testlauf - eine Fernbedienung, die echte Rechnungen ausstellt, hat in einer
Buchhaltung nichts zu suchen. api.py liegt wie immer in beiden Programmen.

Der Erststart fragte nach "der zuletzt vergebenen Nummer" - das holt niemanden
ab, der bei 0 anfaengt. Jetzt zwei Wege: bei 0 anfangen ODER die eigene
Zaehlung weiterfuehren. Unter das schon Vergebene kommt keiner der beiden.

pruef_nummernsperre bediente den Dialog nur halb: nach einer abgelehnten Zahl
bleibt er offen (so soll er), niemand drueckte "Spaeter" - das Programm wartete
in wait_window() ewig. Das war schon vor diesem Umbau so.

Neu: pruef_api.py. Alle 14 Pruefstaende gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
TheMockTv 2026-09-05 18:24:59 +02:00
parent 142dddc647
commit ca9ac212cf
11 changed files with 835 additions and 212 deletions

View file

@ -10,7 +10,6 @@ im Layout der Excel-Vorlage:
"""
import os
import io
import json
from reportlab.lib.pagesizes import A4
@ -19,8 +18,7 @@ from reportlab.lib import colors
from reportlab.lib.enums import TA_RIGHT, TA_LEFT, TA_CENTER
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, KeepTogether, PageBreak,
Flowable,
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, KeepTogether,
)
import bilder
@ -28,23 +26,6 @@ import config
from berechnung import eur, eur_kurz, runde
class _AmtMarke(Flowable):
"""Unsichtbar. Haelt fest, auf welcher Seite das Amt-Blatt beginnt.
Nur so kann die Fusszeile die Kundenseite vom Amt-Blatt unterscheiden - sie wird
gezeichnet, bevor der Seiteninhalt steht, und weiss von sich aus nichts davon.
"""
def __init__(self, merker):
super().__init__()
self._merker = merker
def wrap(self, *_):
return (0, 0)
def draw(self):
self._merker["seite"] = self.canv.getPageNumber()
# ---- Farben / Masse ---------------------------------------------------------
# Die Akzentfarbe gehoert der Firma, nicht dem Programm: Ueberschriften, die
# Linie unter dem Kopf und der Tabellenkopf tragen sie. Ein Handwerker will kein
@ -157,8 +138,8 @@ def _vorschau_marke(canvas):
canvas.restoreState()
def _kopf_fuss(canvas, doc, cfg, amt=False, vorschau=False):
"""Kopf und Fuss einer Seite. amt=True -> Blatt fuer das Amt (siehe Fusszeile)."""
def _kopf_fuss(canvas, doc, cfg, vorschau=False):
"""Kopf und Fuss einer Seite."""
firma = cfg["firma"]
canvas.saveState()
if vorschau:
@ -203,13 +184,8 @@ def _kopf_fuss(canvas, doc, cfg, amt=False, vorschau=False):
canvas.line(MARGIN_L, line_y, PAGE_W - MARGIN_R, line_y)
# --- Fusszeile ---
#
# STRIKTE TRENNUNG Kunde / Amt:
# Kundenseite : voller Briefkopf-Fuss - Bankverbindung und Steuernummer MUESSEN
# drauf, das ist die Rechnung, die der Gast bezahlt.
# Amt-Blatt : KEINE Bankverbindung, KEINE Steuernummer. Das Amt zieht nichts
# ein und ueberweist nichts. Es bleiben Inhaber (wer meldet) und
# die Kontaktdaten fuer Rueckfragen.
# Bankverbindung und Steuernummer MUESSEN drauf: das ist die Rechnung, die der
# Gast bezahlt, und die Steuernummer ist Pflichtangabe (§ 14 Abs. 4 Nr. 2 UStG).
fy = MARGIN_BOT - 4 * mm
canvas.setStrokeColor(LINE)
canvas.setLineWidth(0.6)
@ -220,12 +196,7 @@ def _kopf_fuss(canvas, doc, cfg, amt=False, vorschau=False):
col_m = MARGIN_L + CONTENT_W * 0.40
col_r = PAGE_W - MARGIN_R
# Das Fusszeilen-Bild steht auf dem AMT-BLATT NICHT. Ein fertiges
# Briefpapier-Band traegt fast immer Bank und Steuernummer - und genau die
# duerfen dort nicht auftauchen (siehe Trennung oben). Das Amt bekommt
# deshalb immer die gezeichnete Fusszeile, auch wenn der Nutzer den Haken
# "Firmenangaben stehen im Bild" gesetzt hat.
fuss_img, fiw, fih = (None, 0, 0) if amt else bilder.laden(firma.get("fussbild"))
fuss_img, fiw, fih = bilder.laden(firma.get("fussbild"))
if fuss_img:
# Schmaler Streifen UNTER den Fusstexten. Die Texte bleiben, wo sie sind -
# Firma, Anschrift, Bank und Steuernummer stehen auf jeder Rechnung, egal
@ -234,9 +205,8 @@ def _kopf_fuss(canvas, doc, cfg, amt=False, vorschau=False):
canvas.drawImage(fuss_img, MARGIN_L + (CONTENT_W - dw) / 2, 7 * mm,
width=dw, height=dh, preserveAspectRatio=True, mask="auto")
rows_l = [] if amt else [firma.get("bank", ""), firma.get("iban", ""), firma.get("bic", "")]
rows_m = [firma.get("inhaber", "")] if amt else [firma.get("inhaber", ""),
firma.get("steuernummer", "")]
rows_l = [firma.get("bank", ""), firma.get("iban", ""), firma.get("bic", "")]
rows_m = [firma.get("inhaber", ""), firma.get("steuernummer", "")]
rows_r = [firma.get("homepage", ""), firma.get("email", ""), firma.get("telefon", "")]
for i, t in enumerate(rows_l):
@ -327,12 +297,8 @@ def _montage_zeile(story, st, rechnung, kopf):
story.append(dreier)
def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
"""Baut den kompletten Seiteninhalt (Absender -> Positionen -> Summen -> Danktexte).
gesamtrechnung_betont=True hebt die Zwischensumme-Zeile wie den Gesamtbetrag hervor
(fett + zwei Streifen) -> fuer das Amt-Blatt. Sonst 1:1 die Kundenseite.
"""
def _seite_inhalt(story, st, cfg, rechnung, kopf):
"""Baut den kompletten Seiteninhalt (Absender -> Positionen -> Summen -> Danktexte)."""
# Absenderzeile (klein, unterstrichen)
story.append(Paragraph(
f"<u>{cfg['firma'].get('absender','')}</u>",
@ -428,7 +394,6 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
titel = {"storno": "Stornorechnung",
"berichtigung": "Berichtigte Rechnung",
"angebot": "Unverbindliches Angebot"}.get(art, "Rechnungsübersicht")
titel += " (für das Amt)" if gesamtrechnung_betont else ""
story.append(Paragraph(titel, st["h"]))
if angebot:
# Ohne diesen Satz sieht das Blatt aus wie eine Rechnung. Es ist keine -
@ -477,7 +442,7 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
# Auf dem Angebot wird gar keine Steuer ausgewiesen - weder Umsatzsteuer
# noch Beherbergungssteuer. Es steht nur, was der Gast voraussichtlich
# zahlt; aufgeschluesselt wird erst in der Rechnung.
if rechnung.aufschlaege and not gesamtrechnung_betont and not angebot:
if rechnung.aufschlaege and not angebot:
summen_rows.append([
Paragraph("Zwischensumme:", st["info_l"]),
Paragraph(eur(rechnung.brutto), st["cell_r"]),
@ -489,7 +454,7 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
Paragraph(eur(a.betrag), st["cell_r"]),
])
endsumme = rechnung.brutto if gesamtrechnung_betont else rechnung.gesamt
endsumme = rechnung.gesamt
summen_rows.append([
Paragraph("Voraussichtlicher Gesamtbetrag:" if angebot else
"Rechnungsbetrag Insgesamt:" if montage else "Gesamtbetrag:",
@ -508,14 +473,13 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
summen.setStyle(TableStyle(style_cmds))
story.append(summen)
# Hinweis zur Beherbergungssteuer - nur auf der Kundenseite. Auf dem Amt-Blatt
# wird die Steuer gar nicht mehr ausgewiesen, dann waere der Satz sinnlos.
# Hinweis zur Beherbergungssteuer.
if angebot:
story.append(Spacer(1, 1.5 * mm))
story.append(Paragraph(
"In diesem Betrag sind alle Steuern und Abgaben enthalten. Aufgeschlüsselt "
"werden sie erst in der Rechnung nach dem Aufenthalt.", st["dank"]))
elif not gesamtrechnung_betont:
else:
for a in rechnung.aufschlaege:
if not a.bezeichnung:
continue
@ -534,90 +498,83 @@ def _seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont):
story.append(Spacer(1, 8 * mm))
# ---- Schlusstexte -------------------------------------------------------
# Dank- und Zahlungstexte gehoeren nur auf die Kundenseite. Die
# STORNO-Erklaerung gehoert aber AUCH aufs Amtsblatt: dort steht sonst eine
# Stornonummer neben einer voellig anderen Rechnungsnummer, und niemand
# erfaehrt, warum. Sein Fund am 04.09.2026: *"in der pdf wird der satz nicht
# angezeigt, da ist dann ein sprung in den rechnungs nummern"* - genau das
# Blatt, das die Stadt bekommt, war der Beleg ohne Begruendung.
if not gesamtrechnung_betont or art == "storno":
texte = cfg.get("texte", {})
block = []
if art == "storno":
# Auf einem Storno sind Dank- und Zahlungstexte falsch - hier wird
# nichts geschuldet, sondern aufgehoben.
bezug = kopf.get("storno_zu", "")
if kopf.get("storno_datum"):
bezug += f" vom {kopf['storno_datum']}"
folgt = kopf.get("korrektur_nummer") or ""
if kopf.get("ersatz_vorhanden") and folgt:
# Doppelt geschriebene Leistung: es kommt nichts nach, sie ist
# bereits abgerechnet. Der Satz muss das sagen - sonst wartet der
# Gast auf eine Rechnung, die nie kommt.
block.append(Paragraph(
f"Diese Stornorechnung hebt die Rechnung {bezug} vollständig auf. "
f"Die Leistung wurde versehentlich doppelt in Rechnung gestellt und "
f"ist bereits mit der Rechnung {folgt} abgerechnet – es folgt keine "
"weitere Rechnung. Bereits gezahlte Beträge werden erstattet oder "
"verrechnet.", st["dank"]))
story.append(KeepTogether(block))
return
if kopf.get("nur_storno"):
# Nur Storno (04.09.2026): die Leistung wird gar nicht abgerechnet.
# Hier darf NICHT von einer berichtigten Rechnung die Rede sein -
# es kommt keine, und der Gast wartet sonst darauf.
block.append(Paragraph(
f"Diese Stornorechnung hebt die Rechnung {bezug} vollständig auf. "
"Die Leistung wird nicht abgerechnet – es folgt keine weitere "
"Rechnung. Bereits gezahlte Beträge werden erstattet.", st["dank"]))
story.append(KeepTogether(block))
return
kette = (f" Die berichtigte Rechnung trägt die Nummer {folgt} "
f"(also {kopf.get('storno_zu','')} → {kopf.get('rechnungsnummer','')} "
f"→ {folgt}).") if folgt else ""
texte = cfg.get("texte", {})
block = []
if art == "storno":
# Auf einem Storno sind Dank- und Zahlungstexte falsch - hier wird
# nichts geschuldet, sondern aufgehoben.
bezug = kopf.get("storno_zu", "")
if kopf.get("storno_datum"):
bezug += f" vom {kopf['storno_datum']}"
folgt = kopf.get("korrektur_nummer") or ""
if kopf.get("ersatz_vorhanden") and folgt:
# Doppelt geschriebene Leistung: es kommt nichts nach, sie ist
# bereits abgerechnet. Der Satz muss das sagen - sonst wartet der
# Gast auf eine Rechnung, die nie kommt.
block.append(Paragraph(
f"Diese Stornorechnung hebt die Rechnung {bezug} vollständig auf. "
"Bereits gezahlte Beträge werden erstattet oder mit der berichtigten "
"Rechnung verrechnet." + kette, st["dank"]))
elif art == "berichtigung":
# Der Satz ist kein Beiwerk: mehrere Rechnungen ueber dieselbe Leistung
# ohne Kennzeichnung loesen die Steuer erneut aus (Abschn. 14c.1 Abs. 4
# Satz 4 UStAE). Gegenueber Endverbrauchern entsteht sie zwar nicht
# (EuGH C-378/21, BMF v. 27.02.2024) - aber Firmengaeste und
# gewerbliche Dauercamper gibt es auch, und der Nachweis liegt beim
# Aussteller. Also bleibt der Satz drauf.
f"Die Leistung wurde versehentlich doppelt in Rechnung gestellt und "
f"ist bereits mit der Rechnung {folgt} abgerechnet – es folgt keine "
"weitere Rechnung. Bereits gezahlte Beträge werden erstattet oder "
"verrechnet.", st["dank"]))
story.append(KeepTogether(block))
return
if kopf.get("nur_storno"):
# Nur Storno (04.09.2026): die Leistung wird gar nicht abgerechnet.
# Hier darf NICHT von einer berichtigten Rechnung die Rede sein -
# es kommt keine, und der Gast wartet sonst darauf.
block.append(Paragraph(
f"Dieses Blatt berichtigt die Angaben zur Rechnung "
f"{kopf.get('berichtigt_zu','')} vom {kopf.get('datum','')} nach "
"§ 31 Abs. 5 UStDV. Leistungen, Beträge und Umsatzsteuer sind "
"unverändert – es handelt sich <b>nicht</b> um eine zusätzliche "
"Leistung und nicht um eine zweite Rechnung. Die Rechnung behält "
"ihre Nummer.", st["dank"]))
if texte.get("dank1"):
block.append(Paragraph(texte["dank1"], st["dank"]))
elif angebot:
# Dank- und Zahlungstexte waeren hier falsch: der Aufenthalt liegt
# noch vor uns, und bezahlt wird nichts. Der Hinweis "bitte die
# Rechnungsnummer angeben" wuerde dem Blatt sogar widersprechen.
block.append(Paragraph(
"Dieses Angebot ist freibleibend und unverbindlich (§ 145 BGB) und "
"keine Zahlungsaufforderung. Sagen Sie uns Bescheid, dann reservieren "
"wir – die Rechnung erhalten Sie nach dem Aufenthalt. Für Rückfragen "
"stehen wir gern zur Verfügung.",
st["dank"]))
else:
if art == "korrektur" and kopf.get("ersetzt"):
hinweis = f"Diese Rechnung ersetzt die Rechnung {kopf['ersetzt']}"
if kopf.get("storno_nummer"):
hinweis += f", die mit der Stornorechnung {kopf['storno_nummer']} aufgehoben wurde"
block.append(Paragraph(hinweis + ".", st["dank"]))
if texte.get("dank1"):
block.append(Paragraph(texte["dank1"], st["dank"]))
if texte.get("dank2"):
block.append(Paragraph(texte["dank2"], st["dank"]))
if texte.get("hinweis"):
block.append(Paragraph(texte["hinweis"], st["hinweis"]))
story.append(KeepTogether(block))
f"Diese Stornorechnung hebt die Rechnung {bezug} vollständig auf. "
"Die Leistung wird nicht abgerechnet – es folgt keine weitere "
"Rechnung. Bereits gezahlte Beträge werden erstattet.", st["dank"]))
story.append(KeepTogether(block))
return
kette = (f" Die berichtigte Rechnung trägt die Nummer {folgt} "
f"(also {kopf.get('storno_zu','')} → {kopf.get('rechnungsnummer','')} "
f"→ {folgt}).") if folgt else ""
block.append(Paragraph(
f"Diese Stornorechnung hebt die Rechnung {bezug} vollständig auf. "
"Bereits gezahlte Beträge werden erstattet oder mit der berichtigten "
"Rechnung verrechnet." + kette, st["dank"]))
elif art == "berichtigung":
# Der Satz ist kein Beiwerk: mehrere Rechnungen ueber dieselbe Leistung
# ohne Kennzeichnung loesen die Steuer erneut aus (Abschn. 14c.1 Abs. 4
# Satz 4 UStAE). Gegenueber Endverbrauchern entsteht sie zwar nicht
# (EuGH C-378/21, BMF v. 27.02.2024) - aber Firmengaeste und
# gewerbliche Dauercamper gibt es auch, und der Nachweis liegt beim
# Aussteller. Also bleibt der Satz drauf.
block.append(Paragraph(
f"Dieses Blatt berichtigt die Angaben zur Rechnung "
f"{kopf.get('berichtigt_zu','')} vom {kopf.get('datum','')} nach "
"§ 31 Abs. 5 UStDV. Leistungen, Beträge und Umsatzsteuer sind "
"unverändert – es handelt sich <b>nicht</b> um eine zusätzliche "
"Leistung und nicht um eine zweite Rechnung. Die Rechnung behält "
"ihre Nummer.", st["dank"]))
if texte.get("dank1"):
block.append(Paragraph(texte["dank1"], st["dank"]))
elif angebot:
# Dank- und Zahlungstexte waeren hier falsch: der Aufenthalt liegt
# noch vor uns, und bezahlt wird nichts. Der Hinweis "bitte die
# Rechnungsnummer angeben" wuerde dem Blatt sogar widersprechen.
block.append(Paragraph(
"Dieses Angebot ist freibleibend und unverbindlich (§ 145 BGB) und "
"keine Zahlungsaufforderung. Sagen Sie uns Bescheid, dann reservieren "
"wir – die Rechnung erhalten Sie nach dem Aufenthalt. Für Rückfragen "
"stehen wir gern zur Verfügung.",
st["dank"]))
else:
if art == "korrektur" and kopf.get("ersetzt"):
hinweis = f"Diese Rechnung ersetzt die Rechnung {kopf['ersetzt']}"
if kopf.get("storno_nummer"):
hinweis += f", die mit der Stornorechnung {kopf['storno_nummer']} aufgehoben wurde"
block.append(Paragraph(hinweis + ".", st["dank"]))
if texte.get("dank1"):
block.append(Paragraph(texte["dank1"], st["dank"]))
if texte.get("dank2"):
block.append(Paragraph(texte["dank2"], st["dank"]))
if texte.get("hinweis"):
block.append(Paragraph(texte["hinweis"], st["hinweis"]))
story.append(KeepTogether(block))
def _kenndaten(rechnung, kopf):
@ -736,52 +693,37 @@ def render_rechnung(pfad, cfg, rechnung, kopf, vorschau=False):
rechnung : berechnung.Rechnung
kopf : dict mit rechnungsnummer, datum, anreise, abreise, naechte, kunde_zeilen
EIN PDF mit zwei Blaettern (falls Beherbergungssteuer anfaellt):
Seite 1 = Kunden-Rechnung, vollstaendig (Steuer ausgewiesen, Bankverbindung im Fuss).
Letzte Seite = Blatt fuers Amt: KEINE Beherbergungssteuer (auch nicht im Gesamt),
keine Zwischensumme, keine Bankverbindung, keine Steuernummer.
EIN Blatt: die Rechnung fuer den Gast.
Bis zum 05.09.2026 hing hier ein zweites Blatt "fuer das Amt" mit derselben
Rechnungsnummer, aber ohne Beherbergungssteuer - und damit mit einem anderen
Gesamtbetrag. Genau das ist bei einer Pruefung ein Problem: mehrere Blaetter
zum selben Umsatz mit verschiedenen Endbetraegen sehen aus, als werde ueber
verschiedene Umsaetze abgerechnet. Es ist ersatzlos entfallen.
Was die Stadt braucht, kommt aus dem Steuerjournal: es liest die Kenndaten
(subject) dieser PDF und rechnet die Bemessungsgrundlage selbst aus. Die
Anmeldung geht als Summenmeldung ans Amt, nicht als Rechnungskopie mit
Gastnamen.
"""
_design_anwenden(cfg) # Firmenfarbe VOR den Stilen - sie lesen sie mit
st = _styles()
def bauen(ziel, amt_seite):
doc = SimpleDocTemplate(
ziel, pagesize=A4,
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
title=({"storno": "Stornorechnung",
"angebot": "Angebot"}.get(kopf.get("art"), "Rechnung"))
+ f" {kopf.get('rechnungsnummer','')}",
author=cfg["firma"].get("name", ""),
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
)
story = []
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=False) # Kunde
# Ein Angebot bekommt KEIN Blatt fuers Amt: gemeldet wird eine
# Uebernachtung erst, wenn sie abgerechnet ist.
if rechnung.aufschlaege and kopf.get("art") != "angebot":
story.append(PageBreak())
story.append(_AmtMarke(marke))
_seite_inhalt(story, st, cfg, rechnung, kopf, gesamtrechnung_betont=True) # Amt
doc = SimpleDocTemplate(
pfad, pagesize=A4,
leftMargin=MARGIN_L, rightMargin=MARGIN_R,
topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOT,
title=({"storno": "Stornorechnung",
"angebot": "Angebot"}.get(kopf.get("art"), "Rechnung"))
+ f" {kopf.get('rechnungsnummer','')}",
author=cfg["firma"].get("name", ""),
subject=_kenndaten(rechnung, kopf), # maschinenlesbar fuers Steuerjournal
)
story = []
_seite_inhalt(story, st, cfg, rechnung, kopf)
def seite(c, d):
_kopf_fuss(c, d, cfg,
amt=(amt_seite is not None and c.getPageNumber() >= amt_seite),
vorschau=vorschau)
def seite(c, d):
_kopf_fuss(c, d, cfg, vorschau=vorschau)
doc.build(story, onFirstPage=seite, onLaterPages=seite)
# Zwei Durchlaeufe, und zwar aus einem konkreten Grund:
#
# Kopf und Fuss werden VOR dem Seiteninhalt gezeichnet - beim Zeichnen weiss die
# Seite also noch nicht, ob auf ihr gleich die Kunden- oder die Amts-Seite landet.
# Auf "Seite 2 ist das Amt" zu wetten, waere falsch, sobald die Kundenrechnung
# laenger als eine Seite wird: dann stuende die Bankverbindung ploetzlich auf dem
# Amt-Blatt und fehlte beim Kunden.
#
# Darum: erster Lauf ins Nichts, nur um zu erfahren, auf welcher Seite das
# Amt-Blatt beginnt. Zweiter Lauf schreibt die echte Datei.
marke = {}
bauen(io.BytesIO(), None)
bauen(pfad, marke.get("seite"))
doc.build(story, onFirstPage=seite, onLaterPages=seite)
return pfad