Diergarten – Folien GmbH · Werkzeug-Dokumentation

PPWR-Erklärungen automatisch aus Rechnungen

Ein deterministisches Python-Skript erzeugt je Artikelposition einer Rechnung eine PPWR-Erklärung nach Art. 16 der Verordnung (EU) 2025/40 als PDF — ohne KI im laufenden Betrieb, ohne Abo, ohne Cloud-Dienst.

Stand 29.07.2026 Stack Python 3 · pdfplumber · docxtpl · LibreOffice Tests 46 automatisiert, alle grün Laufende Kosten 0 €
Was das Skript tut

Von der Rechnung zur fertigen Erklärung

Extraktion, Regelwerk und Dokumenterzeugung sind getrennte Module. Deshalb lässt sich die Datenquelle austauschen — heute das Rechnungs-PDF, später die Business-Central-API — ohne dass an Regeln oder Vorlage etwas geändert werden muss.

01 · QUELLE Rechnung einlesen PDF aus Business Central (Text + Koordinaten) oder direkt die BC-REST-API.
02 · EXTRAKTION Felder erkennen Rechnungsnummer, Datum, Bestellnummer, Anschrift, Positionsliste.
03 · REGELN Positionen bewerten Fracht & Rabatte aussortieren, Beschreibungstext vererben.
04 · VORLAGE Word füllen Sechs Platzhalter in template.docx, restlicher Text bleibt unangetastet.
05 · AUSGABE PDF schreiben LibreOffice headless (oder Word), Datei PPWR_<Nr>_Pos<n>.pdf.
06 · PHASE 2 Versand & Archiv SMTP-Versand mit festem Text, Archivkopie unter Archiv/<Jahr>/<Rechnungsnr>/.

Grundsatz: lieber abbrechen als leer ausliefern

Fehlt ein Pflichtfeld — Rechnungsnummer, Datum, Bestellnummer, Kundenanschrift — bricht der Lauf mit einer klaren Meldung ab. Es entsteht in keinem Fall eine Erklärung mit leerem Feld.

Fachliche Regeln

Drei Regeln entscheiden, was erzeugt wird

Eine Erklärung je Artikelposition

Zwei Artikel auf einer Rechnung ergeben zwei PDFs. Die Kopfdaten sind auf allen Erklärungen einer Rechnung identisch, nur der Produktname unterscheidet sich.

Text-Vererbung bei bloßer Abmessung

Steht in einer Position nur noch eine Abmessung („S2 17 × 48789“), übernimmt sie den Beschreibungstext der Position darüber und behält die eigene Abmessung.

Keine Erklärung für Nicht-Ware

Fracht, Verpackungskosten, Rabatte, Skonto, Zwischensummen erzeugen nichts. Beim PDF-Weg über eine erweiterbare Musterliste, über die API über den Zeilentyp.

Zeilenumbrüche im Artikeltext erkennt das Parsing daran, dass in der Pos.-Spalte keine Nummer steht — eine umbrochene Beschreibung erzeugt also keine zusätzliche Erklärung.

Testen

Rechnung prüfen, bevor etwas erzeugt wird

Der Ablauf zeigt immer zuerst, was er gelesen hat. Erst wenn die Felder stimmen, werden PDFs geschrieben. Dieselbe Prüfung gibt es auf der Kommandozeile mit python -m ppwr erzeugen rechnung.pdf --trocken.

Verbindung zum Testserver wird geprüft …
Rechnungs-PDF hierher ziehen oder Datei auswählen — die Datei verlässt den Rechner nicht
Mitgelieferte Testrechnungen

Rechnung rein, Erklärungen raus — zum Nachschauen

Die drei Testrechnungen und die daraus erzeugten Erklärungen zum Öffnen. Der Fixtext der Erklärungen stammt aus einer Muster-Vorlage und ist oben rot als solche gekennzeichnet — er wird durch die Originalvorlage von Diergarten ersetzt.

TestrechnungFallErzeugte Erklärungen
Rechnung 103045 zwei Artikel, dazu eine Frachtzeile Pos1 · Pos2
Rechnung 103046 zwei Positionen ohne Vortext (nur Abmessung), dazu Verpackungskosten und Rabatt Pos1 · Pos2 · Pos3
Rechnung 103047 eine Position, Artikeltext über mehrere Zeilen umbrochen Pos1
Anleitung

Einrichtung auf einem Windows-Rechner

  1. Python und LibreOffice installieren

    Python 3.11 oder neuer von python.org — beim Installieren „Add python.exe to PATH“ anhaken. LibreOffice von de.libreoffice.org erledigt die PDF-Umwandlung. Ist auf dem Rechner Microsoft Word installiert, geht alternativ pip install docx2pdf.

  2. Projekt einrichten

    :: im Projektordner, einmalig
    python -m venv .venv
    .venv\Scripts\pip install -r requirements.txt
  3. Vorlage vorbereiten

    Die Original-Vorlage Template.doc in den Projektordner legen. Das Werkzeug ersetzt die rot markierten Beispieltexte durch Platzhalter und stellt sie auf Schwarz — der übrige Text bleibt unverändert.

    :: 1. ansehen, was rot markiert ist
    .venv\Scripts\python tools\prepare_template.py Template.doc --auflisten
    
    :: 2. umwandeln
    .venv\Scripts\python tools\prepare_template.py Template.doc vorlagen\template.docx
    
    :: 3. gegenprüfen
    .venv\Scripts\python -m ppwr pruefe-vorlage vorlagen\template.docx

    Erkennt die Automatik eine Stelle nicht, trägt man den Platzhalter in Word von Hand ein: {{KUNDENANSCHRIFT}}, {{RECHNUNGSDATUM}}, {{KUNDENNAME}}, {{PRODUKTNAME}}, {{RECHNUNGSNUMMER}}, {{BESTELLNUMMER}}. Steht „Amberg,“ in der Vorlage fest, den Lauf mit --ort "" starten, dann liefert der Platzhalter nur das Datum.

  4. Trockenlauf mit echten Rechnungen

    Zeigt die erkannten Felder als Tabelle und schreibt nichts.

    .venv\Scripts\python -m ppwr erzeugen beispiele --trocken
  5. Erklärungen erzeugen

    .venv\Scripts\python -m ppwr --log ppwr.log erzeugen eingang --archiv Archiv

    Ergebnisse liegen in ausgabe\, die Archivkopie unter Archiv\<Jahr>\<Rechnungsnummer>\. Für den Alltag genügt ein Doppelklick auf PPWR-Erklaerungen.bat: die Datei zeigt erst die Felder und fragt dann nach, bevor sie erzeugt.

  6. Optional: Ordner überwachen

    .venv\Scripts\python -m ppwr ueberwachen eingang --erledigt erledigt --archiv Archiv

    Neue PDFs im Eingangsordner werden verarbeitet und anschließend verschoben.

Wenn etwas nicht klappt

MeldungUrsacheAbhilfe
Rechnungsnummer nicht gefunden Rechnungslayout weicht vom Profil ab python -m ppwr debug rechnung.pdf zeigt alle erkannten Zeilen mit Koordinaten; danach die Suchmuster in ppwr/extract/layout.py anpassen
Positionstabelle nicht gefunden Andere Spaltenüberschrift als „Beschreibung“ Überschrift in beschreibungsspalte ergänzen oder beschreibungsspalte_x fest setzen
kein extrahierbarer Text Rechnung ist ein Scan/Bild-PDF Original-PDF aus Business Central verwenden oder auf die API-Variante wechseln
Vorlage enthält die Platzhalter nicht Platzhalter fehlen oder sind durch Word in mehrere Textteile zerlegt Platzhalter in Word in einem Zug neu tippen (nicht aus Teilen zusammensetzen), dann pruefe-vorlage
Keine PDF-Konvertierung verfügbar LibreOffice nicht im Suchpfad Pfad setzen: PPWR_SOFFICE=C:\Program Files\LibreOffice\program\soffice.exe
Business Central

Fünf Wege, das Ganze zu automatisieren

Vom Doppelklick bis zur ERP-Erweiterung. Die Stufen bauen aufeinander auf — jede ist für sich vollständig, keine muss übersprungen werden.

StufeWegAufwandLaufende Kosten BrauchtBewertung
1 Überwachter Ordner + Aufgabenplanung ≈ 30 Min 0 € PC oder Server, der läuft sofort startklar
2 BC-API abfragen (Pull) 1–2 Std + IT 0 € App-Registrierung in Entra ID empfohlener Endzustand
3 PDF zurück an den Beleg hängen + 1 Std 0 € Stufe 2 löst den Versand elegant
4 Webhook aus BC (Push) 0,5–1 Tag 0 €* erreichbarer HTTPS-Endpunkt nur bei Bedarf „sofort“
5 AL-Erweiterung direkt in BC 3–5 Tage Partner 0 € AL-Entwickler / BC-Partner sauberste, aber teuerste Lösung

* sofern ein bestehender Server genutzt wird. Power Automate und Logic Apps bleiben bewusst außen vor: beide erzeugen laufende Lizenz- bzw. Verbrauchskosten und widersprechen der Vorgabe „kein Abo, keine Cloud-Automation“.

Stufe 1 — Überwachter Ordner, gesteuert über die Windows-Aufgabenplanung

Business Central legt die Rechnungs-PDFs entweder direkt in einen Ordner (Berichtsausgabe) oder sie landen über eine Outlook-Regel dort, die Anhänge aus dem Rechnungsversand speichert. Das Skript verarbeitet, was neu ist.

:: Aufgabe anlegen: täglich, alle 15 Minuten wiederholen
schtasks /create /tn "PPWR-Erklaerungen" /sc minute /mo 15 ^
  /tr "C:\ppwr-generator\.venv\Scripts\python.exe -m ppwr --log C:\ppwr-generator\ppwr.log erzeugen C:\ppwr-generator\eingang --archiv C:\ppwr-generator\Archiv --weiter-bei-fehler" ^
  /ru "%USERNAME%"

Alternativ dauerhaft mitlaufen lassen: python -m ppwr ueberwachen eingang --erledigt erledigt. Vorteil dieser Stufe: kein Eingriff in Business Central, keine Freigabe durch die IT nötig. Nachteil: hängt am Rechnungslayout — ändert sich das Layout, müssen die Suchmuster nachgezogen werden (die Testsuite meldet das sofort).

Stufe 2 — Daten direkt aus der Business-Central-API holen

Der saubere Endzustand: kein PDF-Parsing, kein Layout-Risiko. Zusätzlicher Gewinn — die Regel „keine Erklärung für Fracht und Rabatte“ ergibt sich aus dem Zeilentyp (lineType = "Item") statt aus Textmustern.

Einmalige Einrichtung

  1. In Entra ID (früher Azure AD) eine App-Registrierung anlegen, Client-Secret erzeugen.
  2. API-Berechtigung Dynamics 365 Business Central → Application permissions → API.ReadWrite.All hinzufügen und Administrator-Zustimmung erteilen.
  3. In Business Central die Seite Microsoft Entra-Anwendungen öffnen, die Client-ID eintragen, Status auf Aktiviert setzen und den Berechtigungssatz D365 BASIC (lesend genügt) zuweisen.
  4. Zugangsdaten in die Datei .env eintragen — sie gehört nicht ins Repository.
# Token holen (Client Credentials)
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
grant_type=client_credentials&scope=https://api.businesscentral.dynamics.com/.default

# Rechnungen eines Zeitraums
GET .../v2.0/{environment}/api/v2.0/companies({id})/salesInvoices
    ?$filter=invoiceDate ge 2026-07-01 and status eq 'Open'

# Positionen dazu
GET .../salesInvoices({invoiceId})/salesInvoiceLines
:: Aufruf im Projekt — dieselbe Ausgabe, dieselben Regeln wie beim PDF-Weg
python -m ppwr bc --top 20 --archiv Archiv
python -m ppwr bc --filter "number eq '103046'" --trocken

Das Modul ppwr/extract/bc_api_source.py ist fertig; die Feldzuordnung steht als Dictionary am Anfang der Klasse. Weicht euer Setup ab — etwa Rechnungsanschrift über billTo… statt sellTo… — ändert sich nur diese Zuordnung, kein Ablauf.

Stufe 3 — Die Erklärung als Beleganhang zurück nach Business Central

Statt eine zweite E-Mail zu verschicken, wandert die erzeugte Erklärung als Anhang an die gebuchte Rechnung. Damit ist sie im ERP auffindbar, geht auf demselben Weg wie die Rechnung nach draußen und die Aufbewahrung liegt dort, wo der Beleg ohnehin liegt.

# Anhang anlegen und Inhalt hochladen
POST .../companies({id})/salesInvoices({invoiceId})/attachments
     { "fileName": "PPWR_103046_Pos2.pdf", "parentId": "{invoiceId}" }
PATCH .../attachments({attachmentId})/attachmentContent  # Rohdaten des PDF

Für gebuchte Belege heißt die Route salesInvoices im API-v2.0-Endpunkt bzw. documentAttachments in der Standard-Web-Service-Variante — welche zur Verfügung steht, hängt an eurer BC-Version; das prüft die IT einmalig mit einem Testaufruf.

Stufe 4 — Push statt Abfrage: Webhook aus Business Central

Business Central kann Änderungen an salesInvoices aktiv melden, statt abgefragt zu werden. Die Erklärung entsteht dann Sekunden nach dem Buchen. Nötig ist ein von außen erreichbarer HTTPS-Endpunkt, der die Registrierung beantwortet; Abonnements laufen nach drei Tagen ab und müssen automatisch erneuert werden.

Lohnt sich, wenn ohnehin ein Server vorhanden ist und die Erklärung zeitgleich mit der Rechnung rausgehen soll. Für den Anfang bringt die 15-Minuten-Abfrage aus Stufe 2 dasselbe Ergebnis bei deutlich weniger Technik.

Stufe 5 — AL-Erweiterung: alles bleibt in Business Central

Ein BC-Partner baut die Erklärung als Word-Layout-Bericht in BC nach und hängt einen Subscriber an das Buchen der Verkaufsrechnung. Je Artikelzeile wird der Bericht ausgeführt, als Beleganhang gespeichert und über die Berichtsauswahl mit der Rechnung versendet.

  • Dafür: kein zweites System, kein PC der laufen muss, funktioniert für jeden Mandanten und aus der Cloud heraus.
  • Dagegen: Entwicklungsaufwand beim Partner, jede Textänderung an der Erklärung läuft künftig über einen Entwicklungsvorgang statt über Word.
  • Empfehlung: nur angehen, wenn ohnehin Partner-Budget eingeplant ist — der Nutzen gegenüber Stufe 2 + 3 ist überschaubar.

Empfehlung in einem Satz

Mit Stufe 1 sofort in den Echtbetrieb gehen, parallel bei der IT die App-Registrierung anstoßen und dann auf Stufe 2 + 3 wechseln — damit hängt nichts mehr am Rechnungslayout und die Erklärung liegt am Beleg statt in einem Ordner.

Offene Punkte

Was noch von eurer Seite kommen muss

PunktAktueller StandWas gebraucht wird
Original-Vorlage Muster-Vorlage im Projekt, alle Platzhalter gesetzt, Fixtext nur ein Platzhaltertext Template.doc — danach ein Aufruf von prepare_template.py
Echte Rechnungen Drei erzeugte Testrechnungen im BC-Layout, alle Regeln damit abgesichert 2–3 echte PDFs, davon eins mit mehreren Artikeln und eins mit Position ohne Vortext
Dateibenennung PPWR_<Rechnungsnummer>_Pos<n>.pdf Freigabe — oder Wunschformat, etwa mit Kundenname
„Amberg,“ in der Vorlage Ort wird mitgeliefert, per --ort "" abschaltbar kurze Bestätigung, ob der Ort in der Vorlage fest steht
Format Rechnungsnummer 103046 vom 21.07.2026 Freigabe oder abweichendes Wunschformat
Betriebsort läuft auf jedem Windows-Rechner mit Python Entscheidung: Bürorechner oder Server (bestimmt Stufe 1 gegen Stufe 2)
Versand (Phase 2) SMTP-Versand mit festem Text und Archivkopie ist eingebaut, standardmäßig aus Postfach + Empfängerregel: an die Rechnungsadresse, zusammen mit oder getrennt von der Rechnung