Automation

Vom Termin zum Angebot

Aus einem Gesprächsprotokoll wird ein Angebotsentwurf — mit einer Liste offener Punkte, die über den Zahlen steht. Zwei echte Läufe, zwei echte Fehler, und warum am Ende kein Mail-Knoten steht.

Anton Brinckmann10 Min. Lesezeit

Stand
22. September 2026
Vom Termin zum Angebot
Teilen

Ein Erstgespräch dauert vierzig Minuten. Das Angebot dazu zu schreiben dauert oft länger — nicht weil es schwer wäre, sondern weil es liegen bleibt. Jemand muss die Notizen wiederfinden, die Sätze nachschlagen, die Positionen tippen, die Umsatzsteuer rechnen und das Ganze in die Vorlage bringen. Zwei Tage später fragt der Mandant nach.

Der Teil dazwischen ist mechanisch, und mechanische Arbeit lässt sich abgeben. Was sich nicht abgeben lässt, ist die Entscheidung, was im Angebot steht. Dieser Artikel baut ein Werkzeug, das die mechanische Hälfte übernimmt und die andere sichtbar auf den Tisch legt — als Liste offener Punkte, die oben im Entwurf steht, bevor die erste Zahl kommt.

Was du danach in der Hand hast

  • Ein Skript, das aus der Liste „Besprochen:“ eines Protokolls die Positionen gegen eine Satztabelle aufbaut und Netto, Umsatzsteuer und Gesamt rechnet
  • Einen Entwurf als Text und als PDF, in dem die offenen Punkte über den Zahlen stehen und auf jeder Seite „Entwurf“ steht
  • Eine Satztabelle als CSV, die jeder pflegen kann, der eine Tabelle bedienen kann
  • Einen n8n-Ablauf, der bei einem neuen Protokoll das Skript anstößt und im Mandantenordner endet

Was danach nicht geht: Rabatte und Staffelpreise, ein Anschreiben, eine Prüfung, ob der Preis stimmt, und der Versand. Die Gründe stehen unten.

Voraussetzungen

  • Python 3.12 (geprüft mit 3.12.2) und reportlab für das PDF (pip3 install reportlab, geprüft mit 4.4)
  • Ein Protokoll mit einer Aufzählung unter „Besprochen:“, wie es jemand nach dem Termin in zehn Minuten tippt. Wer stattdessen aus einer Besprechungsmitschrift kommt, findet den Schritt davor im Beitrag zum Protokoll aus der Besprechung
  • Eine Satztabelle mit euren Leistungen. Fünf Beispielzeilen liegen im Paket
  • Optional Ollama mit einem lokalen Modell, wenn ein Protokoll keine Liste hat und ein Modell sie aus dem Fließtext schreiben soll. Bei uns llama3.2
  • Kosten: keine. Der Lauf über ein Protokoll dauert unter einer Sekunde

Der Aufbau

  1. 01

    Protokoll lesen

    Nur die Aufzählung unter „Besprochen:“, dazu die Zeilen „Mandant:“, „Budget:“ und „Nicht im Angebot:“. Alles andere ist Kontext und wird nicht gelesen. Fehlt die Liste, bricht das Skript ab, statt im Fließtext zu raten.

  2. 02

    Punkte der Satztabelle zuordnen

    Jeder Punkt wird über eine Stichwortliste einem Kürzel aus der Satztabelle zugeordnet. Kein Treffer heißt „Keinem Satz zugeordnet“ und wandert nach oben in die offenen Punkte.

  3. 03

    Menge und Einheit prüfen

    Die Menge kommt aus dem Punkt („zwei Tage“, „sechs Monate“). Fehlt sie, wird 1 angesetzt und das steht oben. Nennt der Punkt eine Einheit, die nicht zur Satztabelle passt, wird die Position nicht berechnet, sondern gemeldet. Das ist die Stelle, an der der erste Entwurf falsch lag.

  4. 04

    Entwurf schreiben

    Erst die offenen Punkte, dann die Tabelle, dann Netto, Umsatzsteuer, Gesamt, Gültigkeit. Als Text nach stdout und mit --pdf als PDF mit dem Vermerk auf jeder Seite.

  5. 05

    Ablegen, nicht verschicken

    Der n8n-Ablauf ruft bei einem neuen Protokoll das Skript und legt das PDF in den Mandantenordner. Dort endet er.

Was wir weglassen: kein Anschreiben, keine Preislogik, keine Anbindung an ein Angebotssystem. Der Entwurf ist eine Datei, die ein Mensch öffnet.

Was am Ende herauskommt

Eingabe ist ein Gesprächsprotokoll, wie es jemand nach dem Termin in zehn Minuten tippt. Ausgabe ist das hier — ein echter Lauf, ungekürzt:

ANGEBOTSENTWURF — nicht versandfertig
Mandant: Kanzlei Rieder und Partner, Landshut

Vor dem Versand zu klären:
  - Menge nicht genannt, mit 1 angesetzt: „Aufbau der Erkennung und Zuordnung“
  - Menge nicht genannt, mit 1 angesetzt: „Anbindung an das bestehende DMS (DATEV DMS)“
  - Ausdrücklich nicht im Angebot: Auslesen der Rechnungspositionen, Übergabe an die Buchhaltung.
  - Budgetrahmen wurde im Termin nicht beziffert.

Leistung                                 Menge Einheit             Satz        Summe
------------------------------------------------------------------------------------
Analyse des Ist-Zustands vor Ort             1 Tag             1.450,00     1.450,00
Aufbau Erkennung und Zuordnung               1 Tag             1.450,00     1.450,00
Anbindung an bestehendes System              1 Tag             1.450,00     1.450,00
Schulung der Mitarbeitenden                  1 halber Tag        750,00       750,00
Betreuung nach Übergabe                      3 Monat             480,00     1.440,00
------------------------------------------------------------------------------------
Netto                                                                       6.540,00
Umsatzsteuer 19 %                                                           1.242,60
Gesamt                                                                      7.782,60

Gültig bis 12.10.2026

Das ist kein fertiges Angebot, und es soll auch keins sein. Es ist ein Entwurf mit vier ehrlichen Lücken, den jemand in fünf Minuten zu Ende bringt — statt ihn in zwei Stunden von Null zu schreiben.

Warum die offenen Punkte oben stehen

Die naheliegende Bauweise wäre: Das Skript findet, was es findet, und schreibt ein Angebot. Was es nicht findet, lässt es weg. Das Ergebnis sieht dann vollständig aus und ist es nicht — und genau solche Entwürfe gehen raus, weil niemand vermisst, was er nicht sieht.

Deshalb ist die Reihenfolge umgedreht. Ganz oben steht, was unklar ist; darunter kommen erst die Zahlen. Wer den Entwurf öffnet, sieht zuerst die Arbeit, die noch zu tun ist. Im Kopf jeder Seite und im Dateinamen steht "Entwurf". Ein Werkzeug, das an dieser Stelle zurückhaltend ist, wird benutzt; eins, das sich sicher gibt, wird nach dem ersten Fehler abgeschaltet.

Vier Arten von offenen Punkten kommen vor: eine Leistung wurde besprochen, aber es gibt keinen Satz dafür. Eine Menge wurde nicht genannt. Etwas wurde ausdrücklich für später vertagt. Und der Budgetrahmen ist offen. Alle vier sind Sätze, die sonst jemand im Kopf behalten müsste.

Die Satztabelle ist das Herz, nicht das Skript

Die Preise stehen nicht im Code, sondern in einer CSV-Datei mit vier Spalten. Das ist die wichtigste Entscheidung im ganzen Aufbau, denn sie entscheidet, wer das Werkzeug pflegen kann:

kuerzel;leistung;einheit;satz
analyse;Analyse des Ist-Zustands vor Ort;Tag;1450
aufbau;Aufbau Erkennung und Zuordnung;Tag;1450
anbindung;Anbindung an bestehendes System;Tag;1450
schulung;Schulung der Mitarbeitenden;halber Tag;750
betreuung;Betreuung nach Übergabe;Monat;480

Wer die Sätze anhebt, öffnet eine Tabelle. Wer sie in den Code schreibt, braucht jemanden, der Code ändern kann — und nach einem halben Jahr traut sich niemand mehr dran. Das gilt für fast jede Automatisierung: Alles, was sich im Betrieb ändert, gehört in eine Datei neben das Programm, nicht hinein.

Was aus dem Protokoll gelesen wird

Das Skript sucht nicht im ganzen Text, sondern nur in der Aufzählung unter "Besprochen:". Das ist ein Vertrag mit den Menschen, die das Protokoll schreiben: Was ins Angebot soll, steht in dieser Liste. Alles davor ist Kontext, alles danach ist Nachtrag.

block = re.search(r"Besprochen:\s*(.*?)(?:\n\s*\n|\nNicht besprochen|\nNicht im Angebot)",
                  text, re.S)

# Stichwörter, keine Freitextsuche: was nicht getroffen wird, soll
# auffallen, nicht stillschweigend verschwinden.
STICHWORT = {
    "analyse":   ["analyse des ist-zustands", "ist-aufnahme", "analyse vor ort"],
    "aufbau":    ["aufbau der erkennung", "aufbau der", "einrichtung der"],
    "anbindung": ["anbindung an", "schnittstelle zu"],
    "schulung":  ["schulung"],
    "betreuung": ["betreuung"],
}

Stichwortlisten statt Freitextsuche sind hier Absicht. Eine unscharfe Suche würde "Migration der Altangebote aus Word" irgendwie dem Punkt "Aufbau" zuordnen, weil beides nach Arbeit klingt. Eine Stichwortliste ordnet es nicht zu und schreibt stattdessen "Keinem Satz zugeordnet" in den Entwurf. Das ist die richtige Antwort: Für die Migration gibt es keinen Satz, also muss jemand einen machen.

Die Menge kommt aus einem zweiten Muster über denselben Punkt — "zwei Tage", "sechs Monate", "ein halber Tag". Zahlwörter kommen über eine kleine Tabelle herein, weil sie in Protokollen häufiger stehen als Ziffern.

Der Fehler, der ein falsches Angebot verschickt hätte

Der zweite Testlauf lief über ein anderes Protokoll, und darin stand eine Zeile, die es in sich hatte: „Schulung für alle zehn Berater, ein Tag“. So sah der erste Entwurf dazu aus; das Paket stellt ihn mit --erster-entwurf nach, der Lauf ist vom 18. September 2026:

Vor dem Versand zu klären:
  - Keinem Satz zugeordnet: „Migration der Altangebote aus Word“
  - Ausdrücklich nicht im Angebot: Anbindung an das CRM.

Leistung                                 Menge Einheit             Satz        Summe
------------------------------------------------------------------------------------
Analyse des Ist-Zustands vor Ort             2 Tag              1450,00      2900,00
Aufbau Erkennung und Zuordnung              10 Tag              1450,00     14500,00
Schulung der Mitarbeitenden                  1 halber Tag        750,00       750,00
Betreuung nach Übergabe                      6 Monat             480,00      2880,00
------------------------------------------------------------------------------------
Netto                                                                       21030,00
Umsatzsteuer 19 %                                                            3995,70
Gesamt                                                                      25025,70

Zwei Fehler in einem Block. Der erste steht in der Zeile mit der Schulung.

def gleiche_einheit(aus_text, aus_tabelle):
    if aus_text is None:
        return True                # nichts genannt, nichts zu vergleichen
    return EINHEIT.get(aus_text) == EINHEIT.get(aus_tabelle.strip().lower())

# im Aufbau der Positionen:
if not gleiche_einheit(einheit, s["einheit"]):
    offen.append(f"Einheit im Termin ({einheit}) weicht vom Satz ({s['einheit']}) ab, "
                 f"Position nicht berechnet: „{punkt}“")
    continue

So sieht derselbe Lauf nach der Korrektur aus:

ANGEBOTSENTWURF — nicht versandfertig
Mandant: Bauer Elektrotechnik GmbH, Ergolding

Vor dem Versand zu klären:
  - Keinem Satz zugeordnet: „Migration der Altangebote aus Word“
  - Einheit im Termin (tag) weicht vom Satz (halber Tag) ab, Position nicht berechnet: „Schulung für alle zehn Berater, ein Tag“
  - Ausdrücklich nicht im Angebot: Anbindung an das CRM.

Leistung                                 Menge Einheit             Satz        Summe
------------------------------------------------------------------------------------
Analyse des Ist-Zustands vor Ort             2 Tag             1.450,00     2.900,00
Aufbau Erkennung und Zuordnung              10 Tag             1.450,00    14.500,00
Betreuung nach Übergabe                      6 Monat             480,00     2.880,00
------------------------------------------------------------------------------------
Netto                                                                      20.280,00
Umsatzsteuer 19 %                                                           3.853,20
Gesamt                                                                     24.133,20

Gültig bis 12.10.2026

Das Angebot ist jetzt um 750 Euro kleiner und um einen offenen Punkt reicher. Das ist der richtige Tausch.

Der zweite Fehler war kleiner und trotzdem peinlich

Im ersten Lauf standen alle Beträge unter zehntausend, und die Zahlenausgabe sah gut aus. Im zweiten stand dann „21030,00“ im Angebot, im Block oben zu sehen. Der Tausenderpunkt hat gefehlt, weil er nie gebraucht wurde. Das ist kein Rechenfehler, aber ein Angebot mit „21030,00“ sieht nach Bastelei aus, und Angebote werden auch danach beurteilt, wie sie aussehen. Die Korrektur ist eine Zeile, die aus 1450.0 ein 1.450,00 macht; im Paket heißt sie betrag.

Die Lehre ist nicht, dass Zahlenformatierung schwierig wäre. Sie ist, dass ein einziger Testlauf nichts beweist. Der zweite Fall, mit anderen Größenordnungen und anderen Formulierungen, hat beide Fehler gefunden — ein Fall hätte keinen gefunden.

Das PDF

Der Entwurf geht danach in ein PDF, gebaut mit reportlab, und dort wiederholt sich der Grundsatz von oben: Der Block mit den offenen Punkten steht auf Seite eins vor der Positionstabelle, hinterlegt und umrandet, nicht als Fußnote. Im Kopf jeder Seite steht „ENTWURF — vor Versand prüfen“, im Fuß noch einmal. Wer das PDF ausdruckt und weiterreicht, kann nicht übersehen, dass es keins zum Verschicken ist.

ENTWURF = "ENTWURF — vor Versand prüfen"

def kopf_und_fuss(canvas, doc):
    # Auf jeder Seite oben rechts und unten links: das ist keins zum Verschicken.
    canvas.saveState()
    canvas.setFont("Helvetica-Bold", 9)
    canvas.setFillColor(colors.Color(0.7, 0.24, 0.16))
    canvas.drawRightString(A4[0] - 20 * mm, A4[1] - 15 * mm, ENTWURF)
    canvas.drawString(20 * mm, 12 * mm, ENTWURF)
    canvas.restoreState()

SimpleDocTemplate(str(pfad), pagesize=A4).build(teile, onFirstPage=kopf_und_fuss, onLaterPages=kopf_und_fuss)

Die beiden PDFs aus den Läufen oben liegen im Paket. Ein Wort zu Sonderzeichen: Die eingebaute Schrift Helvetica kann Umlaute und ß, aber keinen Gedankenstrich in jeder Fassung; wer im PDF ein Kästchen statt des Strichs sieht, bindet eine TrueType-Schrift ein. In unseren zwei PDFs vom 18. September ist der Strich da.

Erst wenn ein Mensch die offenen Punkte abgearbeitet hat, wird aus dem Entwurf ein Angebot — und das ist dann ein anderer Schritt mit einer anderen Vorlage, in der der Entwurfsvermerk nicht mehr vorkommt. Diese Trennung sauber zu halten, ist mehr wert als jede Verbesserung der Erkennung.

Wie das in n8n aussieht

Der Ablauf hängt in n8n an einem Ordner, wenn ihr das Skript nicht von Hand aufrufen wollt. Der Export im Paket hat sechs Knoten, und keiner davon enthält Logik, die nicht auch im Skript steht:

  • Local File Trigger: neue Datei im Ordner „Protokolle“, mit awaitWriteFinish, damit halb geschriebene Dateien nicht anstoßen.
  • Ein Set-Knoten mit den Pfaden: Protokoll, Skript, Ablageordner, PDF-Name mit „_ENTWURF“ im Namen.
  • Ein Execute-Command-Knoten, der angebot.py mit --pdf aufruft. Derselbe Code wie oben, nicht noch einmal in JavaScript.
  • Ein IF-Knoten auf den Exit-Code.
  • Zwei Enden: „Entwurf liegt im Mandantenordner“ und „Fehler: Protokoll prüfen“, meist weil die Liste „Besprochen:“ fehlt. An beide gehört eine interne Meldung an die Person, die den Termin hatte — und ausdrücklich kein Mail-Knoten, der den Entwurf an den Mandanten schickt.

Wir haben den Code aus dem Skript absichtlich nicht in einen Code-Knoten übertragen. Zwei Fassungen derselben Regeln laufen nach einem halben Jahr auseinander, und dann rechnet n8n anders als das Skript, mit dem man den Fehler sucht.

Der letzte Punkt in der Liste ist wichtiger als die anderen. Die Versuchung, direkt einen Versand anzuhängen, ist groß, weil es genau ein Knoten mehr wäre. Genau dieser eine Knoten macht aus einem hilfreichen Werkzeug eins, das euch ein falsches Angebot zum Mandanten schickt, während ihr im nächsten Termin sitzt.

Was das nicht macht

  • Es hört nicht zu. Die Eingabe ist ein getipptes Protokoll. Aus einer Aufzeichnung ein Protokoll zu machen, ist ein eigener Schritt — und einer, bei dem die Frage nach der Einwilligung der Gesprächspartner vor der Technik kommt.
  • Es kennt keine Rabatte, Pauschalen oder Staffelpreise. Sobald eure Angebote Preislogik haben, gehört die in die Satztabelle, nicht in Ausnahmen im Code.
  • Es schreibt keinen Text. Die Einleitung, die Leistungsbeschreibung und die Bedingungen kommen aus eurer Vorlage. Das Skript füllt die Tabelle, nicht das Anschreiben.
  • Es prüft nicht, ob der Preis stimmt. Ob ein Projekt mit zehn Tagen Aufbau realistisch kalkuliert ist, steht in keinem Protokoll.
  • Es ersetzt die Durchsicht nicht. Die offenen Punkte sind eine Hilfe beim Lesen, keine Erlaubnis, nicht zu lesen.

Wenn das Protokoll keine Liste hat

Manche schreiben nach dem Termin keine Aufzählung, sondern drei Absätze Fließtext. Dafür hat das Skript den Schalter --freitext: Ein lokales Modell bekommt das Protokoll mit der Anweisung aus prompts/positionen_aus_protokoll.md und schreibt die Liste „Besprochen:“ im Wortlaut des Protokolls, mit Mengen nur dort, wo sie stehen. Die Liste wird angezeigt und dann genauso behandelt wie eine getippte, mit denselben Stichwörtern und derselben Einheitenprüfung.

Am 18. September haben wir das mit llama3.2 über eine Fließtextfassung des ersten Protokolls laufen lassen. Die Liste hatte fünf Punkte und eine Ausschlusszeile, alle richtig, und der Entwurf war derselbe wie aus der getippten Liste. Dazu zwei Einschränkungen: Der Fließtext ähnelte dem Beispiel in der Anweisung, das ist ein leichter Fall. Und das Modell hat „Betreuung nach der Übergabe“ zu „Betreuung“ gekürzt, was hier egal war und bei einer Stichwortliste mit längeren Wendungen einen Treffer kosten kann. Die Liste ist deshalb auf dem Bildschirm zu sehen, bevor der Entwurf kommt.

Zum Nachbauen

Das Paket zu diesem Beitrag enthält das Skript, die Satztabelle, die zwei Protokolle mit den erzeugten PDFs, die Anweisung für Fließtext und den n8n-Export. Entpacken, reportlab installieren, dann:

  • angebot.py: das Skript aus diesem Beitrag, vollständig. Mit --pdf für das PDF, mit --erster-entwurf für die zwei Fehler, mit --freitext für Protokolle ohne Liste
  • satztabelle.csv: die fünf Beispielsätze. Hier stehen die Preise, nicht im Code
  • testdaten/protokoll_1.txt und protokoll_2.txt: die zwei Protokolle, mit denen die Läufe oben entstanden sind, dazu entwurf_1.pdf und entwurf_2.pdf
  • prompts/positionen_aus_protokoll.md: die Anweisung für den Fließtextfall
  • n8n/termin_zum_angebot.json: der Ablauf mit sechs Knoten und ohne Versand

Vom Termin zum Angebot: Skript, Satztabelle, zwei Testprotokolle, n8n-Ablauf

ZIP · 14,9 kB

Python-Skript, das aus einem Gesprächsprotokoll einen Angebotsentwurf mit offenen Punkten oben und PDF (reportlab) macht, Satztabelle als CSV, die zwei Testprotokolle aus dem Beitrag mit den erzeugten PDFs, der Prompt für Fließtext-Protokolle und der n8n-Ablauf ohne Mail-Knoten.

Herunterladen

Stand dieses Artikels

Beide Protokolle sind selbst erzeugte Testdateien mit Formulierungen, wie sie in echten Gesprächsnotizen vorkommen, keine echten Mandantengespräche. Die Sätze in der Tabelle sind Beispielwerte. Alle abgedruckten Läufe sind echte Läufe über diese Dateien, am 18. September 2026 mit Python 3.12.2 und reportlab 4.4, mit festem Stichtag 12.09.2026. Die PDFs im Paket stammen aus denselben Läufen. Der Fließtextfall ist einmal mit llama3.2 (3B) geprüft. Nicht geprüft: der n8n-Export gegen ein laufendes n8n; die Knoten sind nach der Exportstruktur geschrieben, die Pfade sind Platzhalter.

Die beiden beschriebenen Fehler sind im ersten Entwurf tatsächlich aufgetreten und nicht zur Veranschaulichung erfunden. Sie sind der Grund, warum in diesem Artikel zwei Testprotokolle vorkommen und nicht eins, und warum das Skript sie mit --erster-entwurf jederzeit wieder zeigen kann.

Häufige Fragen

Muss ich dafür programmieren können?

Für Änderungen an den Stichwörtern ja, ein wenig; das ist eine Liste oben im Skript. Für alles, was sich im Betrieb ändert, nicht: Die Preise liegen in einer Tabelle, die jeder ändern kann, der eine Tabelle bedienen kann. Der n8n-Ablauf ruft das fertige Skript auf.

Was passiert, wenn eine besprochene Leistung keinen Satz hat?

Sie wird nicht ins Angebot geschrieben und steht stattdessen unter „Vor dem Versand zu klären“ mit dem Wortlaut aus dem Protokoll. Das ist Absicht: Eine fehlende Position, die oben benannt ist, wird bearbeitet. Eine, die stillschweigend weggelassen wurde, fehlt im verschickten Angebot.

Lohnt sich das bei fünf Angeboten im Monat?

Für die Zeitersparnis allein wahrscheinlich nicht. Der Nutzen liegt woanders: Der Entwurf entsteht am Tag des Termins statt zwei Wochen später, und die offenen Punkte stehen schriftlich fest, solange das Gespräch noch frisch ist. Das wirkt auf die Abschlussquote, nicht auf die Stundenzahl.

  • Belege an DATEV übergeben

    Belege an DATEV übergeben

    Der Zugang ist niedriger, als sein Ruf vermuten lässt — eine Beraternummer braucht es nicht, die Sandbox kommt mit dem Abonnement. Und elf Stunden Token-Gültigkeit entscheiden über den ganzen Aufbau.

  • Ein Agent, der eure Ablage kennt

    Ein Agent, der eure Ablage kennt

    Das meiste daran ist Suche, nicht Modell. Eine Volltextsuche über die eigene Ablage mit Fundstelle und Begründung — und der Satz, der schlimmer ist als eine leere Trefferliste.

  • Fristen aus einem Vertragsstapel

    Fristen aus einem Vertragsstapel

    Ein Skript liest sechs Vertragsklauseln und rechnet zurück, bis wann eine Kündigung zugehen müsste. Drei Verträge rechnet es, drei nicht — und die drei, die es liegen lässt, sind der interessantere Teil.

© 2026 ernsthaft.ai, ein Angebot der abi group GmbH

Alle genannten Produktnamen, Logos und Marken sind Eigentum der jeweiligen Inhaber. Ihre Nennung bedeutet keine Empfehlung oder Verbindung zu ernsthaft.ai oder der abi group GmbH.

Build v1.10.0+e670d45