Rechnungs-Download und Buchhaltung
Frühere Version vom 2026-07-16T17:47:07.006334+02:00 · zur aktuellen Fassung
Rechnungs-Download und Buchhaltung
Orientierung für interessierte Laien
Der Prozess holt Rechnungen aus dem WEtell-Kundenkonto und übergibt sie an Lexoffice. WEtell ist die Quelle, Lexoffice das Buchhaltungsziel. Die PDF bleibt zusätzlich lokal als Originalbeleg erhalten.
Die Code-Links zeigen auf die jeweilige Datei in der `/code`-Ansicht. Die genannten Klassen und Methoden sind die darin enthaltenen Symbole. Die Architektur trennt fachliche Regeln von technischen Adaptern: Ein Use Case beschreibt, was fachlich geschieht; ein Port beschreibt eine benötigte Fähigkeit; ein Adapter verbindet diese Fähigkeit mit WEtell, Lexoffice, PDF-Werkzeugen oder Dateien.
1. Prozess starten und orchestrieren
`run-rechnung.php` lädt die Konfiguration für WEtell und Lexoffice, baut die Adapter zusammen und startet den zentralen Ingestions-Use-Case. Ein Use Case ist ein klar abgegrenzter Anwendungsvorgang. Das Ergebnis wird als JSON ausgegeben.
Relevante Symbole
- `run-rechnung.php` als Programmeinstieg: https://demo.karlkratz.com/code/105
- `IngestInvoicesUseCase::__construct`: https://demo.karlkratz.com/code/109
- `IngestInvoicesUseCase::execute`: https://demo.karlkratz.com/code/109
- `IngestResult::__construct`: https://demo.karlkratz.com/code/110
- `IngestResult::toArray`: https://demo.karlkratz.com/code/110
- `InvoiceProcessException::__construct`: https://demo.karlkratz.com/code/111
2. Bei WEtell anmelden und Rechnungen entdecken
Der Prozess meldet sich beim WEtell-Konto an und liest die verfügbare Rechnungsübersicht. Der Headless-Adapter steuert dafür einen Browser ohne sichtbare Oberfläche. Die bestehende Playwright-Instanz wird über den Node.js-Server verwendet; ein eigener Chromium-Aufruf ist nicht erforderlich.
Wenn der bevorzugte Zugang nicht funktioniert, kann der konfigurierte Fallback-Adapter einen alternativen Zugang verwenden. Entdeckte Rechnungen werden als `SourceInvoice` beschrieben. Diese fachliche Beschreibung enthält unter anderem Quell-ID, Rechnungsnummer-Hinweis, Datumshinweis und Download-URL.
Relevante Symbole
- `DiscoverInvoicesUseCase::__construct`: https://demo.karlkratz.com/code/106
- `DiscoverInvoicesUseCase::execute`: https://demo.karlkratz.com/code/106
- `WetellSourcePort::login`: https://demo.karlkratz.com/code/128
- `WetellSourcePort::discoverInvoices`: https://demo.karlkratz.com/code/128
- `HeadlessWetellSourceAdapter::__construct`: https://demo.karlkratz.com/code/132
- `HeadlessWetellSourceAdapter::login`: https://demo.karlkratz.com/code/132
- `HeadlessWetellSourceAdapter::discoverInvoices`: https://demo.karlkratz.com/code/132
- `FallbackWetellSourceAdapter::__construct`: https://demo.karlkratz.com/code/131
- `FallbackWetellSourceAdapter::login`: https://demo.karlkratz.com/code/131
- `FallbackWetellSourceAdapter::discoverInvoices`: https://demo.karlkratz.com/code/131
- `HttpWetellSourceAdapter::__construct`: https://demo.karlkratz.com/code/133
- `HttpWetellSourceAdapter::login`: https://demo.karlkratz.com/code/133
- `HttpWetellSourceAdapter::discoverInvoices`: https://demo.karlkratz.com/code/133
- `HttpWetellSourceAdapter::request`: https://demo.karlkratz.com/code/133
- `HttpWetellSourceAdapter::extractLoginPayload`: https://demo.karlkratz.com/code/133
- `HttpWetellSourceAdapter::extractFromHtml`: https://demo.karlkratz.com/code/133
- `HttpWetellSourceAdapter::normalizeUrl`: https://demo.karlkratz.com/code/133
- `SourceInvoice::__construct`: https://demo.karlkratz.com/code/126
- `SourceInvoice::rechnungsschluessel`: https://demo.karlkratz.com/code/126
- `wetell-headless/server.js::withBrowser`: https://demo.karlkratz.com/code/142
- `wetell-headless/server.js::loginAndGetPage`: https://demo.karlkratz.com/code/142
- `wetell-headless/server.js::doLoginOnly`: https://demo.karlkratz.com/code/142
- `wetell-headless/server.js::doDiscover`: https://demo.karlkratz.com/code/142
- `wetell-headless/server.js::parseJsonBody`: https://demo.karlkratz.com/code/142
- `wetell-headless/server.js::sendJson`: https://demo.karlkratz.com/code/142
3. Rechnung herunterladen und Original speichern
Die gefundene Download-URL wird nicht ungeprüft als Beleg akzeptiert. Der Dateiinhalt wird geladen und als PDF validiert. Danach wird die Originaldatei datums- und lieferantenbezogen unter `rechnungen/originale/` gespeichert. Der SHA-256-Wert ist ein digitaler Fingerabdruck der gespeicherten Datei.
Relevante Symbole
- `DownloadInvoiceUseCase::__construct`: https://demo.karlkratz.com/code/107
- `DownloadInvoiceUseCase::execute`: https://demo.karlkratz.com/code/107
- `DownloadInvoiceUseCase::isPdf`: https://demo.karlkratz.com/code/107
- `WetellSourcePort::downloadInvoicePdf`: https://demo.karlkratz.com/code/128
- `HeadlessWetellSourceAdapter::downloadInvoicePdf`: https://demo.karlkratz.com/code/132
- `FallbackWetellSourceAdapter::downloadInvoicePdf`: https://demo.karlkratz.com/code/131
- `HttpWetellSourceAdapter::downloadInvoicePdf`: https://demo.karlkratz.com/code/133
- `InvoiceFileStoragePort::buildTargetPath`: https://demo.karlkratz.com/code/118
- `InvoiceFileStoragePort::persistDownloadedPdf`: https://demo.karlkratz.com/code/118
- `InvoiceFileStoragePort::sha256`: https://demo.karlkratz.com/code/118
- `DateBasedInvoiceStorage::__construct`: https://demo.karlkratz.com/code/129
- `DateBasedInvoiceStorage::buildTargetPath`: https://demo.karlkratz.com/code/129
- `DateBasedInvoiceStorage::persistDownloadedPdf`: https://demo.karlkratz.com/code/129
- `DateBasedInvoiceStorage::sha256`: https://demo.karlkratz.com/code/129
- `DateBasedInvoiceStorage::sanitize`: https://demo.karlkratz.com/code/129
- `DateBasedInvoiceStorage::normalizeDate`: https://demo.karlkratz.com/code/129
- `wetell-headless/server.js::doDownload`: https://demo.karlkratz.com/code/142
- `wetell-headless/server.js::cryptoHash`: https://demo.karlkratz.com/code/142
- `rechnungen/wetell-downloads.json`: https://demo.karlkratz.com/code/144
4. PDF-Inhalt extrahieren und fachlich prüfen
Nach dem Download wird die PDF gelesen. Zuerst wird der normale Textextraktor verwendet. Ist der Textlayer unbrauchbar oder fehlt er, greift der Fallback über OCR. OCR bedeutet Texterkennung aus dem Bild einer Seite.
Aus dem PDF werden Lieferant, Anschrift, echte Rechnungsnummer, Rechnungsdatum sowie Beträge gelesen. Die fachlichen Prüfungen stellen sicher, dass Pflichtfelder vorhanden sind und die Beträge plausibel zusammenpassen. Die Rechnungsnummer stammt aus dem PDF, nicht aus einer technischen Datei-ID.
Relevante Symbole
- `ExtractInvoiceDataUseCase::__construct`: https://demo.karlkratz.com/code/108
- `ExtractInvoiceDataUseCase::execute`: https://demo.karlkratz.com/code/108
- `TextExtractionPort::extract`: https://demo.karlkratz.com/code/127
- `FallbackTextExtractor::__construct`: https://demo.karlkratz.com/code/130
- `FallbackTextExtractor::extract`: https://demo.karlkratz.com/code/130
- `PdftotextExtractor::__construct`: https://demo.karlkratz.com/code/137
- `PdftotextExtractor::extract`: https://demo.karlkratz.com/code/137
- `PdftotextExtractor::parseText`: https://demo.karlkratz.com/code/137
- `PdftotextExtractor::extractPattern`: https://demo.karlkratz.com/code/137
- `PdftotextExtractor::extractAddress`: https://demo.karlkratz.com/code/137
- `TesseractInvoiceExtractor::__construct`: https://demo.karlkratz.com/code/139
- `TesseractInvoiceExtractor::extract`: https://demo.karlkratz.com/code/139
- `TesseractInvoiceExtractor::parseText`: https://demo.karlkratz.com/code/139
- `TesseractInvoiceExtractor::extractPattern`: https://demo.karlkratz.com/code/139
- `TesseractInvoiceExtractor::extractAddressLine`: https://demo.karlkratz.com/code/139
- `ExtractedInvoiceData::__construct`: https://demo.karlkratz.com/code/116
- `ExtractedInvoiceData::hasRequiredFields`: https://demo.karlkratz.com/code/116
- `ExtractedInvoiceData::hasPlausibleAmount`: https://demo.karlkratz.com/code/116
- `ExtractedInvoiceData::toArray`: https://demo.karlkratz.com/code/116
- `Amount::__construct`: https://demo.karlkratz.com/code/114
- `Amount::isConsistent`: https://demo.karlkratz.com/code/114
- `Amount::toArray`: https://demo.karlkratz.com/code/114
- `ClockPort::now`: https://demo.karlkratz.com/code/115
- `SystemClock::__construct`: https://demo.karlkratz.com/code/138
- `SystemClock::now`: https://demo.karlkratz.com/code/138
5. Duplikate und idempotente Wiederholung behandeln
Vor der Buchung wird geprüft, ob dieselbe Quellrechnung oder derselbe fachliche Rechnungsschlüssel schon bekannt ist. Ein zweiter Lauf mit derselben Rechnung soll nicht zu einem zweiten Lexoffice-Beleg führen. Das nennt man idempotentes Verhalten: Wiederholen ist sicher, weil das Ergebnis nicht doppelt angelegt wird.
Relevante Symbole
- `IngestInvoicesUseCase::execute`: https://demo.karlkratz.com/code/109
- `IngestInvoicesUseCase::shouldProcess`: https://demo.karlkratz.com/code/109
- `IngestInvoicesUseCase::deriveRunStatus`: https://demo.karlkratz.com/code/109
- `InvoiceState::__construct`: https://demo.karlkratz.com/code/121
- `InvoiceState::findBySourceId`: https://demo.karlkratz.com/code/121
- `InvoiceState::findByRechnungsschluessel`: https://demo.karlkratz.com/code/121
- `InvoiceState::withRecord`: https://demo.karlkratz.com/code/121
- `InvoiceState::all`: https://demo.karlkratz.com/code/121
- `InvoiceState::toArray`: https://demo.karlkratz.com/code/121
- `InvoiceStatePort::load`: https://demo.karlkratz.com/code/122
- `InvoiceStatePort::save`: https://demo.karlkratz.com/code/122
- `JsonInvoiceStateAdapter::__construct`: https://demo.karlkratz.com/code/134
- `JsonInvoiceStateAdapter::load`: https://demo.karlkratz.com/code/134
- `JsonInvoiceStateAdapter::save`: https://demo.karlkratz.com/code/134
- `SourceInvoice::rechnungsschluessel`: https://demo.karlkratz.com/code/126
- `InvoiceStatus`: https://demo.karlkratz.com/code/123
- `InvoiceErrorCode`: https://demo.karlkratz.com/code/117
- `InvoiceRecord::withStatus`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::withRetry`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::toArray`: https://demo.karlkratz.com/code/120
- `rechnungen/rechnungen.json`: https://demo.karlkratz.com/code/143
6. Lieferantenkontakt in Lexoffice finden oder anlegen
Der Lieferant wird nicht fest im Programm kodiert. Die extrahierten Daten werden verwendet, um in Lexoffice nach einem passenden Kontakt zu suchen. Wird einer gefunden, wird seine ID wiederverwendet. Andernfalls wird ein neuer Kontakt mit Name und Adresssignatur angelegt.
Relevante Symbole
- `ResolveVendorUseCase::__construct`: https://demo.karlkratz.com/code/113
- `ResolveVendorUseCase::execute`: https://demo.karlkratz.com/code/113
- `LexofficePort::findContactByName`: https://demo.karlkratz.com/code/124
- `LexofficePort::createContact`: https://demo.karlkratz.com/code/124
- `LexofficeApiAdapter::__construct`: https://demo.karlkratz.com/code/136
- `LexofficeApiAdapter::findContactByName`: https://demo.karlkratz.com/code/136
- `LexofficeApiAdapter::createContact`: https://demo.karlkratz.com/code/136
- `LexofficeApiAdapter::request`: https://demo.karlkratz.com/code/136
- `ExtractedInvoiceData::hasRequiredFields`: https://demo.karlkratz.com/code/116
- `InvoiceRecord::withContact`: https://demo.karlkratz.com/code/120
7. Rechnungsbeleg in Lexoffice anlegen
Mit der echten Rechnungsnummer, dem Rechnungsdatum, den extrahierten Beträgen und der Kontakt-ID wird der Lexoffice-Beleg erstellt. Danach wird die geprüfte lokale PDF als Anhang hochgeladen. Der Vorgang gilt erst als erfolgreich, wenn eine gültige Beleg-ID vorliegt und der Datei-Upload erfolgreich bestätigt wurde.
Relevante Symbole
- `PostInvoiceUseCase::__construct`: https://demo.karlkratz.com/code/112
- `PostInvoiceUseCase::execute`: https://demo.karlkratz.com/code/112
- `LexofficePort::postInvoice`: https://demo.karlkratz.com/code/124
- `LexofficeApiAdapter::postInvoice`: https://demo.karlkratz.com/code/136
- `LexofficeApiAdapter::request`: https://demo.karlkratz.com/code/136
- `LexofficeApiAdapter::uploadAttachment`: https://demo.karlkratz.com/code/136
- `PostingResult::__construct`: https://demo.karlkratz.com/code/125
- `PostingResult::toArray`: https://demo.karlkratz.com/code/125
- `InvoiceRecord::withPosting`: https://demo.karlkratz.com/code/120
- `ExtractedInvoiceData::toArray`: https://demo.karlkratz.com/code/116
- `Amount::toArray`: https://demo.karlkratz.com/code/114
8. Status und Protokoll schreiben
Jeder wichtige Schritt wird protokolliert. Der dauerhafte fachliche Zustand liegt in `rechnungen.json`; die einzelnen Logeinträge helfen bei der zeitlichen Nachvollziehbarkeit. Ein Status wie `POSTED` bedeutet, dass der Beleg erfolgreich verarbeitet wurde. Fehler und Wiederholungsversuche bleiben als Zustand erkennbar.
Relevante Symbole
- `InvoiceRecord::withDownload`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::withExtraction`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::withContact`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::withPosting`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::withError`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::withRetry`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::toArray`: https://demo.karlkratz.com/code/120
- `InvoiceState::withRecord`: https://demo.karlkratz.com/code/121
- `InvoiceState::toArray`: https://demo.karlkratz.com/code/121
- `JsonInvoiceStateAdapter::save`: https://demo.karlkratz.com/code/134
- `InvoiceLoggerPort::logStep`: https://demo.karlkratz.com/code/119
- `JsonLineLogger::__construct`: https://demo.karlkratz.com/code/135
- `JsonLineLogger::logStep`: https://demo.karlkratz.com/code/135
- `IngestResult::toArray`: https://demo.karlkratz.com/code/110
- `rechnungen/rechnungen.json`: https://demo.karlkratz.com/code/143
- `rechnungen/wetell-downloads.json`: https://demo.karlkratz.com/code/144
9. Fehler behandeln und sicher wiederholen
Ein Fehler bei Anmeldung, Entdeckung, Download, PDF-Auslesung, Kontaktabgleich oder Lexoffice-Buchung wird dem jeweiligen Schritt zugeordnet. Der Lauf wird nicht fälschlich als vollständig erfolgreich markiert. Bei einem erneuten Lauf entscheiden Status, Quell-ID, Rechnungsschlüssel und Hash erneut, ob eine Rechnung verarbeitet werden muss.
Relevante Symbole
- `InvoiceProcessException`: https://demo.karlkratz.com/code/111
- `InvoiceErrorCode`: https://demo.karlkratz.com/code/117
- `IngestInvoicesUseCase::execute`: https://demo.karlkratz.com/code/109
- `IngestInvoicesUseCase::shouldProcess`: https://demo.karlkratz.com/code/109
- `IngestInvoicesUseCase::deriveRunStatus`: https://demo.karlkratz.com/code/109
- `InvoiceRecord::withError`: https://demo.karlkratz.com/code/120
- `InvoiceRecord::withRetry`: https://demo.karlkratz.com/code/120
- `InvoiceState::findBySourceId`: https://demo.karlkratz.com/code/121
- `InvoiceState::findByRechnungsschluessel`: https://demo.karlkratz.com/code/121
- `JsonLineLogger::logStep`: https://demo.karlkratz.com/code/135
- `PostingResult::toArray`: https://demo.karlkratz.com/code/125
10. Nachweise des Gesamtergebnisses
Ein erfolgreicher Durchlauf hinterlässt drei zusammengehörige Nachweise:
- die geprüfte Original-PDF unter `rechnungen/originale/`,
- den lokalen Status in `rechnungen/rechnungen.json`,
- den Lexoffice-Kontakt und den Beleg mit PDF-Anhang.
Die fachliche Verarbeitung wird durch Application-Use-Cases gesteuert. Die technischen Details bleiben austauschbar: WEtell kann über Headless-Browser, HTTP oder Fallback angebunden werden; die PDF-Auslesung kann über `pdftotext` oder OCR erfolgen; Lexoffice wird über den API-Adapter angesprochen.
Relevante Symbole
- `run-rechnung.php`: https://demo.karlkratz.com/code/105
- `IngestInvoicesUseCase::execute`: https://demo.karlkratz.com/code/109
- `IngestResult::toArray`: https://demo.karlkratz.com/code/110
- `InvoiceRecord::toArray`: https://demo.karlkratz.com/code/120
- `JsonInvoiceStateAdapter::save`: https://demo.karlkratz.com/code/134
- `LexofficeApiAdapter::postInvoice`: https://demo.karlkratz.com/code/136
- `LexofficeApiAdapter::uploadAttachment`: https://demo.karlkratz.com/code/136
- `DateBasedInvoiceStorage::persistDownloadedPdf`: https://demo.karlkratz.com/code/129
- `rechnungen/rechnungen.json`: https://demo.karlkratz.com/code/143
- `rechnungen/wetell-downloads.json`: https://demo.karlkratz.com/code/144
- `wetell-headless/server.js`: https://demo.karlkratz.com/code/142