80 KiB
80 KiB
Changelog
2026-07-27 – Fix: DOM-XSS in der CSV/XLSX-Vorschau beim Bankimport (Issue #133)
BankController::detectFormat()liefert die aus einer hochgeladenen Bank-Umsatzdatei geparsteheader_row/first_dataunverändert als JSON zurück;src/Views/bank/upload.phprendert diese Werte perinnerHTMLin eintitle="..."-Attribut. Die dortigeesc()-Funktion escapte bisher nur&,<und>, nicht aber". Ein Header-/Zellwert wieSpalte" onmouseover="alert(document.cookie)konnte damit aus dem Attribut ausbrechen und beliebigen JavaScript-Code im Kontext des eingeloggten Nutzers ausführen (CWE-79), z.B. um das im<meta>-Tag liegende CSRF-Token auszulesen. Bank-Exportdateien stammen typischerweise von Dritten (Bank, per E-Mail zugeschickter Kontoauszug) und sind damit ein realistischer, angreiferkontrollierter Eingabekanal.src/Views/bank/upload.php:esc()escaped jetzt zusätzlich"(zu") und'(zu').tests/66_BankUploadPreviewXssTest.php: Neuer Test prüft per Quelltext-Check, dassesc()Anführungszeichen escaped, und führt die extrahierte JS-Funktion (fallsnodeverfügbar ist) mit einem bösartigen Zellwert aus, um zu verifizieren, dass kein Ausbruch aus demtitle-Attribut mehr möglich ist.
2026-07-27 – Fix: Reflektiertes XSS über _error-Parameter bei Rechnungs-Zahlungserfassung (Issue #132)
InvoiceController::book()übernahm den Fehler-Array ungeprüft direkt aus$_GET['_error']und reichte ihn an die View weiter. Da_errornirgendwo serverseitig befüllt wird und der CSRF-Schutz nur für POST/PUT/DELETE/PATCH greift, konnte ein Angreifer per einfachem Link (GET /invoices/{id}/book?_error[x]=<script>...) beliebiges JavaScript im Kontext eines eingeloggten Nutzers ausführen und darüber auch das im<meta>-Tag liegende CSRF-Token auslesen.src/Controllers/InvoiceController.php:book()übernimmt_errornicht mehr aus$_GET,$formErrorsstartet immer leer.src/Views/invoices/book.php,src/Views/belege/detail.php: Die Fehleranzeige escaped Schlüssel und Werte jetzt konsequent mit$h()(zuvor ungeschütztesimplode/array_map-Pattern, inbelege/detail.phpbetrifft dies POST-basierte Fehler ausBookingController).tests/65_InvoiceBookReflectedXssTest.php: Neuer Test prüft, dass ein<script>-Payload in$_GET['_error']nicht mehr unescaped in der Ausgabe vonInvoiceController::book()erscheint.
2026-07-27 – Fix: CSV-Formel-Injection im Anlagelisten-Export (Issue #131)
EuerController::anlageCsv()schriebassets.bezeichnung(frei editierbarer Text) ungefiltert perfputcsv()in die exportierteAnlageliste-{year}.csv. Eine Bezeichnung wie=cmd|'/c calc'!A1konnte beim Öffnen der CSV in Excel/LibreOffice zu Formelausführung führen (CSV-/Formel-Injection, CWE-1236).BelegController/BookingControllerwaren dank eigener, privatercsvSanitize()-Methoden bereits abgesichert.src/Controllers/BaseController.php: Die bisher inBelegControllerundBookingControllerdupliziertecsvSanitize()-Methode wurde als gemeinsameprotectedMethode hierher verschoben (Präfix'bei führendem=,+,-oder@).src/Controllers/EuerController.php:anlageCsv()sanitisiertbezeichnungjetzt über$this->csvSanitize().src/Controllers/BelegController.php,src/Controllers/BookingController.php: Duplizierte privatecsvSanitize()-Methoden entfernt, nutzen jetzt die geerbte Methode ausBaseController.tests/64_AnlageCsvInjectionTest.php: Neuer Test prüft, dass eine Anlagenbezeichnung mit führendem=im CSV-Export mit vorangestelltem'maskiert wird.
2026-07-27 – Fix: SSRF-Schutz per DNS-Rebinding umgehbar (TOCTOU) (Issue #130)
SsrfGuard::assertPublicHttpsUrl()löste den Hostnamen einmalig auf und prüfte die IPs, der eigentliche Request (cURL bzw.php-fints) löste den Hostnamen aber später erneut selbst auf. Ein Angreifer mit Kontrolle über die DNS-Antworten der konfigurierten Domain konnte zwischen Prüfung und Request von einer öffentlichen auf eine interne IP (127.0.0.1, internes Netz, Cloud-Metadata) umschalten und so die SSRF-Prüfung aus #127/#118 umgehen.src/Services/SsrfGuard.php: Neue MethoderesolvePinned()gibt zusätzlich zur Validierung die geprüften IP-Adressen alsCURLOPT_RESOLVE-Einträge (HOST:PORT:IP) zurück;assertPublicHttpsUrl()bleibt als Wrapper bestehen.src/Services/NextcloudImportService.php: Der Konstruktor pinnt die geprüfte(n) IP(s) dernextcloud_urleinmalig;newCurl()setztCURLOPT_RESOLVE, sodass alle cURL-Requests (PROPFIND/GET/MOVE/DELETE) den Hostnamen nicht erneut über den System-Resolver auflösen.src/Services/FinTsFetchService.php:php-fintslöst den Hostnamen selbst auf (kein IP-Pinning möglich) – die SSRF-Prüfung wird daher zusätzlich unmittelbar vor dem ersten tatsächlichen Request (nachFinTs::new()) wiederholt, um das Zeitfenster für DNS-Rebinding zu minimieren.tests/63_SsrfPinningTest.php: Neuer Test prüftSsrfGuard::resolvePinned()(Einträge für Hostnamen, leeres Ergebnis + weiterhin Ablehnung für IP-Literale) sowie das Pinning inNextcloudImportService.
2026-07-27 – Fix: CSRF-Schutz für GET /bank/direkt/{id}/run (Issue #136)
- Der SSE-Endpunkt
GET /bank/direkt/{id}/runlöste einen echten Bankabruf (PayPal/FinTS) allein anhand der Session aus, ohne CSRF-Prüfung – daEventSourcenurGETunterstützt, griff die bestehende CSRF-Middleware (nur fürPOST/PUT/DELETE/PATCH) nicht. Ein Angreifer konnte per Top-Level-Navigation (SameSite=Lax) einen eingeloggten Nutzer dazu bringen, unbeabsichtigt einen Abruf für eine beliebige Verbindungs-ID auszulösen. src/Auth.php:validateCsrfToken()prüft jetzt über die neue, seiteneffektfreieisValidCsrfToken()(analog zucookieParams()), damit sich die Vergleichslogik isoliert testen lässt.src/Controllers/BankController.php:direktRun()validiert vor dem eigentlichen Abruf das percsrf_token-Query-Parameter übergebene Token gegen die Session.src/Views/bank/fints.php: DieEventSource-URL hängt das bereits vorhandene, in der Seite eingebettete CSRF-Token (meta[name="csrf-token"]) als Query-Parameter an.tests/62_BankDirektRunCsrfTest.php: Neuer Test prüftAuth::isValidCsrfToken()gegen korrektes, falsches, leeres und fehlendes Session-Token.
2026-07-27 – Feature: Netto/Brutto-Umschalter in der Buchungsmaske (Issue #137)
- Beim Erfassen einer Buchung musste bisher immer der Nettobetrag eingegeben werden – lag nur der Bruttobetrag vor (z. B. von einem Kassenbon), führte die manuelle Rückrechnung regelmäßig zu Rundungsdifferenzen.
src/Views/belege/detail.php: Das Label „Nettobetrag (€)" wurde durch zwei Umschalt-Buttons „NETTOBETRAG (€)" / „BRUTTOBETRAG (€)" ersetzt (setBetragModus()); der jeweils aktive Button ist optisch eingedrückt hervorgehoben und bestimmt, wie der eingegebene Betrag interpretiert wird. Die Live-Anzeige (updateNettoAnzeige()) zeigt je nach Modus die jeweils fehlenden Werte (Netto/USt. bzw. USt./Brutto) an. Beim Absenden des Formulars wird ein im Brutto-Modus eingegebener Betrag serverseitig weiterhin als Nettobetrag benötigt – die Umrechnung erfolgt daher unmittelbar vor dem Submit per JavaScript, sodass Backend (BookingController,Models/Booking.php) unverändert bleibt.public/static/css/app.css: Neue Klasse.betrag-modus-btn(inkl..active-Zustand) für den Umschalter.tests/61_BetragModusToggleTest.php: Neuer Test prüft Vorhandensein der Umschalt-Buttons, der zugehörigen JS-Funktionen sowie der Umrechnung vor dem Absenden.
2026-07-27 – Feature: Lieferanten-Typeahead in der Belegliste (Issue #138)
- Beim Klick in die Spalte „Lieferant" der Belegliste (Inline-Bearbeitung) gab es bisher nur ein freies Textfeld – Lieferanten konnten dadurch versehentlich mit unterschiedlicher Schreibweise mehrfach angelegt werden.
src/Controllers/BelegController.php:index()übergibt jetzt zusätzlichSupplier::all()an die Viewbelege/list.src/Views/belege/list.php: Neue<datalist id="bl-supplier-datalist">mit allen vorhandenen Lieferanten; das Inline-Eingabefeld (blInlineSupplier()) verweist perlist-Attribut darauf – analog zur bereits bestehenden Typeahead-Auswahl in der Buchungsmaske (belege/detail.php).tests/60_BelegListeLieferantTypeaheadTest.php: Neuer Test prüft, dass Datalist, Befüllung und Verknüpfung mit dem Inline-Eingabefeld vorhanden sind.
2026-07-27 – Fix: Auto-Reload-Button der Ereignisanzeige durch iOS-artigen Toggle-Switch ersetzt (Issue #139)
- Rückmeldung zu #139: Der Text-Button „Auto-Reload: An/Aus" war nicht intuitiv genug.
src/Views/ereignisse/nextcloud_import.php: Button durch einen runden Schiebeschalter (Checkbox + CSS, keine externe Bibliothek) ersetzt – links/grau = aus, rechts/grün = ein. Verhalten (Polling,localStorage-Persistenz) unverändert.tests/58_EreignisAutoReloadTest.php: Test an die neue Toggle-Switch-Struktur (Checkbox statt Button,change- stattclick-Event) angepasst.
2026-07-26 – Feature: Dritte Eingabezeile ohne USt. in der Essensbeleg-Buchungsvorlage (Issue #140)
- Die Buchungsvorlage „Essensbeleg n. abzugsfähig (nur USt.)" hatte nur Eingabezeilen für 7 % und 19 % USt. – Beträge ohne USt. (z. B. Trinkgeld) ließen sich damit nicht erfassen.
migrations/088_buchungsvorlage_essensbeleg_ohne_ust.sql: Ergänzt die Vorlage um ein drittesust_zeile-Feldnetto_0(0 % USt.) sowie eine Buchungsregel, die den eingegebenen Betrag auf Konto 4657 („Nicht abzugsfähige Bewirtungskosten ohne Vorsteuer") bucht – Formular-Rendering (buchungsvorlagen/modal.php), Live-Berechnung (app.js) und Buchungsausführung (VorlageBuchungsService) sind bereits vollständig datengetrieben und benötigten keine Code-Änderung.tests/59_EssensbelegOhneUstTest.php: Neuer Test prüft das neue Feld, die zugehörige Regel und die korrekte Buchung auf Konto 4657 bei gleichzeitiger Nutzung aller drei Zeilen.
2026-07-26 – Feature: Ereignisanzeige aktualisiert sich automatisch (Issue #139)
- Die Ereignisanzeige (
/ereignisse/nextcloud-import) war statisch – neu geloggte Ereignisse erschienen erst nach manuellem Neuladen der Seite. src/Controllers/EventController.php: Neue MethodenextcloudImportData()liefert die aktuellen Ereignisse als JSON (Anzahl + serverseitig gerendertes HTML-Fragment), damit initiale Seite und Auto-Reload denselben Rendering-Code (ereignisse/_nextcloud_import_body.php) verwenden.src/Views/ereignisse/_nextcloud_import_body.php: Tabellen-/Leer-Markup ausnextcloud_import.phpin ein Partial ausgelagert, das von initialer Seite und Datenendpunkt gemeinsam genutzt wird.src/Views/ereignisse/nextcloud_import.php: Neuer Ein-/Aus-Button oben rechts in der Kartenkopfzeile schaltet automatisches Neuladen persetInterval(alle 60s, ohne externe Bibliothek) ein/aus; Zustand wird perlocalStoragegemerkt. Ein echter Push-Mechanismus (z. B. Server-Sent Events) hätte dauerhafte PHP-FPM-Worker gebunden – bei diesem Anwendungsfall (Log-Anzeige) ist periodisches Polling die robustere Lösung.public/index.php: Neue RouteGET /ereignisse/nextcloud-import/data.tests/58_EreignisAutoReloadTest.php: Neuer Test prüft Button, Polling-Code, Partial-Wiederverwendung und Routen-Registrierung.
2026-07-21 – Fix: Zwei dauerhaft fehlschlagende Tests (Session-Cookie-Härtung, Nextcloud-SSRF)
- Test 54 (Session-Cookie-Härtung): Der Test rief echtes
Auth::startSession()auf. Datests/run.phpErgebnisse laufend perechoausgibt und Test 54 der einzige Test ist, der Sessions anfasst, war zum Testzeitpunkt bereits Output gesendet – PHP lehntsession_set_cookie_params()/session_start()danach mit „headers already sent" ab (Warnung + wirkungslos). Ein nachträglicher Output-Buffer im Test half nicht, da bereits vor dem Test unbuffered Output geflossen war.src/Auth.php: Neue reine MethodeAuth::cookieParams()berechnet die gehärteten Cookie-Attribute (HttpOnly, SameSite, Secure inkl. Override/Proxy-Erkennung) ohne Seiteneffekt;startSession()nutzt sie jetzt intern. Verhalten zur Laufzeit unverändert.tests/54_SessionCookieHardeningTest.php: Prüft jetztAuth::cookieParams()direkt statt eine echte Session zu starten – unabhängig vom bereits erfolgten Output der Test-Suite.
- Test 56 (Nextcloud-SSRF):
SsrfGuard::assertPublicHttpsUrl()löst den Hostnamen bewusst per echtem DNS auf (Schutz vor DNS-Rebinding). Der Test nutztehttps://cloud.example.comals „gültiges" Beispiel – diese Subdomain ist nicht registriert (NXDOMAIN), wurde also fälschlich als ungültig abgelehnt.tests/56_NextcloudSsrfTest.php: Nutzt stattdessen die laut IANA dauerhaft real auflösbare Apex-Domainhttps://example.com.
2026-07-21 – Feature: Kontostand nach Direktabruf auf der Bank-Verbindungskachel
migrations/087_bank_sources_last_balance.sql: Neue Spaltelast_balanceinbank_sources.src/Models/BankSource.php:updateLastFetched()nimmt optional den beim Abruf ermittelten Kontostand entgegen und persistiert ihn; ohne neuen Wert bleibt der zuletzt bekannte Stand erhalten (kein Verlust bei Fehlern während des Abrufs).src/Services/FinTsFetchService.php: Fragt pro Konto zusätzlich per dedizierterGetBalance-Aktion (HKSAL) den aktuellen gebuchten Saldo ab und summiert diesen. Fällt nur auf den Schlusssaldo des letzten Statements zurück, wenn die Saldenabfrage fehlschlägt – der ursprüngliche Ansatz (Saldo nur aus dem Statement ableiten) lieferte keinen Wert, wenn die Bank mangels neuer Buchungen kein MT940/CAMT-Statement zurückgab (häufigster Fall bei täglichen Abrufen ohne neue Umsätze).src/Services/PayPalNvpFetchService.php: Der bereits vorhandeneGetBalance-Aufruf intestCredentials()liefert den aktuellen Kontostand, der nun mitgespeichert und im Abruf-Log ausgegeben wird. Live-Konten liefern dabei offenbar durchgängig das ListenformatL_AMT0/L_CURRENCYCODE0statt der laut NVP-Doku primären FelderAMT/CURRENCYCODE– beide Formate werden jetzt ausgewertet.src/Views/bank/fints.php: Kachel zeigt bei vorhandenem Kontostand rechts neben dem Abrufdatum den Betrag an (gleiche Höhe wie das Datum), mit dem Label „Kontostand" rechtsbündig darüber. Kachelgröße unverändert.tests/57_BankSourceLastBalanceTest.php: Neuer Test prüft, dasslast_balancebeim Abruf gesetzt wird und bei einem Abruf ohne ermittelten Kontostand erhalten bleibt.
2026-07-16 – Fix: Stored XSS über KI-Kontierungsvorschlag (supplier_name) in Beleg-Detailansicht (Issue #129)
- Mittel-Hoch:
supplier_nameaus dem per LLM erzeugten Kontierungsvorschlag (KontierungsvorschlagService) wird nurtrim()t und ungeprüft inbelege.ki_vorschlag(JSON) gespeichert._showKiBadgeDone()insrc/Views/belege/detail.phpbaute daraus String-Konkatenation und wies sie ohne Escaping überinnerHTMLzu. Da der Belegtext (PDF/Bild/ZUGFeRD-XML) von einem externen Rechnungssteller stammt, konnte ein präparierter Beleg das LLM per Prompt-Injection dazu bringen, fürsupplier_nameHTML/JS-Payload statt eines Firmennamens auszugeben – dieser wurde beim Öffnen des Belegs im Browser-Kontext des eingeloggten Nutzers ausgeführt (CWE-79). src/Views/belege/detail.php:_showKiBadgeDone()verwendet nun die bereits vorhandene_esc()-Hilfsfunktion, umv.supplier_namevor der Einbettung ininnerHTMLHTML-zu-escapen. Andere Ausgabepfade vonsupplier_name/buchungstext(.value,.textContent,.dataset) waren bereits sicher.
2026-07-12 – Fix: SSRF über ungeprüftes nextcloud_url-Feld beim Belegimport (Issue #127)
- Mittel: Anders als bei
fints_url(Issue #118) wurde die in den Einstellungen gespeichertenextcloud_urlweder beim Speichern noch bei der Verwendung geprüft.NextcloudImportServicebaute daraus ungefiltert die WebDAV-Basis-URL für PROPFIND-, GET- und MOVE/DELETE-Requests samt Basic-Auth-Credentials; ein authentifizierter Benutzer mit Zugriff auf die Einstellungen konnte so interne Ziele (127.0.0.1, internes Netz, Cloud-Metadata-Endpunkt169.254.169.254) über den Belegimport-Cron ansprechen (CWE-918). Zusätzlich folgte der verwendete cURL-Handle HTTP-Redirects, wodurch eine nachträgliche Host-Prüfung per Weiterleitung hätte umgangen werden können. src/Controllers/SettingsController.php:update()validiertnextcloud_urlvor dem Speichern mit dem bereits fürfints_urlverwendetenSsrfGuard::assertPublicHttpsUrl(); ungültige Ziele werden mit Fehlermeldung abgelehnt statt in der Datenbank zu landen.src/Services/NextcloudImportService.php: Der Konstruktor prüft die geladenenextcloud_urldefensiv erneut mitSsrfGuard(Schutz gegen DNS-Rebinding und gegen Altbestände, die vor diesem Fix gespeichert wurden); bei Ablehnung bleibtbaseUrlleer undimport()bricht sofort mit Fehlermeldung ab, ohne einen Request abzusetzen.newCurl()setztCURLOPT_FOLLOWLOCATIONnun auffalse, damit eine Weiterleitung die Prüfung nicht umgehen kann.src/Views/settings/form.php: Fehleranzeige für das Nextcloud-URL-Feld ergänzt.tests/56_NextcloudSsrfTest.php: Neuer Test prüft, dasshttp://, private/loopback/link-local IP-Literale sowielocalhostalsnextcloud_urlvonNextcloudImportServiceabgelehnt werden (kein Request,import()meldet Fehler), während gültigehttps://-Ziele weiterhin funktionieren.
2026-07-11 – Fix: E-Mail-Header-Injection über Kunden-Betreffvorlage mail_subject_template (Issue #126)
- Mittel: Das pro Kunde konfigurierbare Feld
mail_subject_templatewurde beim Speichern nurtrim()t, eingebettete\r/\nblieben erhalten. Beim Rechnungsversand landete der Wert ungeprüft im SMTP-Subject:-Header (SmtpMailer::buildMessage()), sodass ein authentifizierter Benutzer über das Kunden-Bearbeitungsformular per CRLF-Payload (z.B.Rechnung %RNR%\r\nBcc: angreifer@example.com) zusätzliche Header injizieren und den Firmen-SMTP-Account zum verdeckten Versand an beliebige Empfänger missbrauchen konnte (CWE-93, analog zu #117, dort aber nur für Empfänger-/CC-Felder behoben). src/Models/Customer.php:create()/update()entfernen\r/\nausmail_subject_templatejetzt zentral über die neue MethodesanitizeSubjectTemplate(), bevor der Wert in der Datenbank landet.src/Services/SmtpMailer.php:encodeHeader()entfernt zusätzlich (Defense-in-Depth)\r/\naus jedem Header-Wert (Subject, From-Name, Anhang-Dateiname), bevor dieser in die rohe SMTP-DATA eingebettet wird – wirksam unabhängig davon, woher der Wert stammt.tests/55_MailSubjectHeaderInjectionTest.php: Neuer Test prüft, dass CRLF-Payloads in der Betreff-Vorlage beim Speichern entfernt werden und dassSmtpMailer::encodeHeader()CR/LF auch bei direkter Übergabe entfernt.
2026-07-10 – Fix: update.sh meldet Testfehler per Textsuche statt Exit-Code (Issue #124)
- Niedrig:
scripts/update.shprüfte den Erfolg der Test-Suite nicht über den Exit-Code vontests/run.php, sondern pergrepnach den Wörtern „FEHLER“, „FAILED“, „fehlgeschlagen“ bzw. „failed“ im Test-Output. Diese Wörter kommen jedoch auch in Testdaten und erwarteten Fehlermeldungen bestandener Tests vor (z.B. Test 47 EventLog, Test 50 SMTP), sodass trotz „✓ Alle Tests bestanden“ eine falsche Warnung ausgegeben wurde. Zusätzlich warTEST_EXIT=${PIPESTATUS[0]:-0}wirkungslos, daPIPESTATUSsich nicht auf die vorangegangene Command-Substitution bezieht. scripts/update.sh: Exit-Code vondocker compose exec ... tests/run.phpwird nun direkt überif TEST_OUTPUT=$(...); then ... else TEST_EXIT=$?; fierfasst (kompatibel mitset -e) und für die Erfolgsmeldung ausgewertet, statt den Output nach Schlüsselwörtern zu durchsuchen.
2026-07-10 – Fix: Session-Cookie ohne Secure-, HttpOnly- und SameSite-Attribute (Issue #121)
- Mittel-Hoch:
session_start()wurde an acht Stellen (src/Auth.php,src/View.php,src/Controllers/LoginController.php) unkonfiguriert aufgerufen;session.cookie_httponly,session.cookie_secureundsession.cookie_samesiteliefen auf den PHP-Compiled-in-Defaults (aus/leer). Das Session-Cookie – Zugriff auf sämtliche Mandanten-/Steuerdaten – war damit per JavaScript auslesbar (volle Session-Übernahme bei jeder Art von XSS) und unterlag keinerSameSite-Beschränkung (CWE-614/CWE-1004). src/Auth.php: Neue zentrale MethodeAuth::startSession()setzt vor jedemsession_start()session_set_cookie_params()mithttponly => trueundsamesite => 'Lax'.securewird anhand des Requests erkannt (HTTPS-Server-Variable bzw.X-Forwarded-Proto: httpshinter einem TLS-terminierenden Reverse-Proxy), damit der Standard-Quickstart per HTTP ohne vorgeschalteten Proxy weiter funktioniert. Über die UmgebungsvariableSESSION_COOKIE_SECURElässt sich das Verhalten explizit erzwingen (1) oder abschalten (0).src/View.php,src/Controllers/LoginController.php: Alle direktensession_start()-Aufrufe durchAuth::startSession()ersetzt, damit die Härtung an jeder Stelle greift..env.example:SESSION_COOKIE_SECUREdokumentiert.tests/54_SessionCookieHardeningTest.php: Neuer Test prüft HttpOnly/SameSite, automatische Secure-Erkennung über HTTPS bzw.X-Forwarded-Proto, sowie den expliziten Override perSESSION_COOKIE_SECURE.
2026-07-10 – Fix: Kein Schutz gegen Brute-Force am Login (Issue #122)
- Mittel:
LoginController::authenticate()prüfte Benutzername/Passwort ausschließlich perpassword_verify()– ohne Rate-Begrenzung, Lockout, Captcha oder Delay. Da/loginals einzige Route bewusst ohneAuth::require()erreichbar ist und Passwörter nur eine Mindestlänge von 8 Zeichen benötigen, war ein Online-Brute-Force-/Credential-Stuffing-Angriff gegen einzelne Konten praktisch unlimitiert (CWE-307). migrations/086_login_throttle.sql: Neue Spaltenusers.failed_attemptsundusers.locked_until.src/Models/User.php:registerFailedLogin()zählt Fehlversuche pro Benutzer und sperrt das Konto ab 5 Fehlversuchen für ein mit jedem weiteren Fehlversuch steigendes Zeitfenster (2^(Fehlversuche-5)Minuten, gedeckelt auf 60 Minuten).resetFailedLogins()setzt den Zähler bei erfolgreichem Login zurück,isLocked()prüft die Sperre.src/Controllers/LoginController.php:authenticate()weist Login-Versuche für gesperrte Konten ab, bevor das Passwort geprüft wird, und registriert Fehlversuche bei falschem Passwort.tests/53_LoginThrottleTest.php: Neuer Test prüft Zählung der Fehlversuche, Sperre ab dem 5. Fehlversuch, steigendes Zeitfenster bei weiteren Fehlversuchen und Reset bei erfolgreichem Login.
2026-07-08 – Fix: SMTP-/Nextcloud-Zugangsdaten im Klartext gespeichert und im HTML sichtbar (Issue #119)
- Hoch:
mail_smtp_pass(SMTP-Passwort) undnextcloud_pass(Nextcloud-App-Passwort) wurden unverschlüsselt incompany_settingsabgelegt, obwohl für vergleichbar sensible Felder (FinTS-PIN, Client-ID/Secret) bereitsCrypto::encrypt()/Crypto::decrypt()(AES-256-GCM) etabliert ist. Zusätzlich schriebsrc/Views/settings/form.phpbeide Klartext-Passwörter in dasvalue-Attribut zurück, wodurch sie für jeden Betrachter des Seitenquelltexts sichtbar waren. src/Models/Setting.php:get()entschlüsseltnextcloud_pass/mail_smtp_passnun überCrypto::decrypt()(Migrationsmodus: bestehende Klartextwerte in der DB werden unverändert durchgereicht),update()verschlüsselt beide Felder vor dem Speichern überCrypto::encrypt()– analog zum bestehenden Muster inBankSource.php.src/Controllers/SettingsController.php:update()behält bei leer gelassenem Passwortfeld das bisherige (entschlüsselte) Passwort bei, statt es zu überschreiben – analog zum bestehenden Muster inBankController.src/Views/settings/form.php:value-Attribute der beiden Passwortfelder entfernt; stattdessen Platzhalter „(gespeichert – leer lassen für unverändert)", wenn bereits ein Wert existiert.migrations/085_encrypt_settings_passwords.sql:mail_smtp_passvonVARCHAR(255)aufTEXTerweitert, damit der Verschlüsselungs-Overhead (Prefix + Base64) nicht abgeschnitten wird.tests/52_SettingsPasswordEncryptionTest.php: Neuer Test prüft, dass beide Felder verschlüsselt in der DB landen, überSetting::get()korrekt entschlüsselt werden und bei leerem Wert nicht verschlüsselt gespeichert werden.
2026-07-08 – Fix: SSRF über ungeprüftes fints_url-Feld bei Bankverbindungen (Issue #118)
- Kritisch: Beim Anlegen/Bearbeiten einer FinTS-Bankverbindung wurde die vom Benutzer eingegebene
fints_urlungefiltert übernommen und später vonFinTsFetchServicedirekt als Ziel-URL für eine echte HTTP-Verbindung verwendet – ein authentifizierter Benutzer konnte den Server so Requests gegen beliebige Ziele absetzen lassen (interne Dienste,localhost, Cloud-Metadata-Endpunkte, CWE-918). - Neu
src/Services/SsrfGuard.php:assertPublicHttpsUrl()erzwingt Schemahttps://, verbietet Userinfo in der URL und löst den Host auf, um sämtliche zugehörigen IPs gegen private/reservierte Bereiche zu prüfen (RFC1918, Loopback, Link-Local/Metadata viaFILTER_FLAG_NO_PRIV_RANGE/FILTER_FLAG_NO_RES_RANGE); bei Verstoß wirft die Methode eineRuntimeException. src/Controllers/BankController.php:direktStore()unddirektUpdate()validierenfints_urlvor dem Speichern mitSsrfGuard; ungültige Ziele werden mit Fehlermeldung abgelehnt statt in der Datenbank zu landen.src/Services/FinTsFetchService.php:run()prüft die gespeicherte URL zusätzlich unmittelbar vor dem Verbindungsaufbau erneut (Schutz gegen DNS-Rebinding zwischen Speichern und Abruf).tests/51_SsrfGuardTest.php: Neuer Test prüft, dasshttp://, private/loopback/link-local IP-Literale,localhostund URLs mit Userinfo abgelehnt werden, während öffentlichehttps://-Ziele weiterhin akzeptiert werden.
2026-07-08 – Fix: SMTP-/E-Mail-Header-Injection über ungeprüfte Empfänger-/CC-Adressen (Issue #117)
- Kritisch: Empfänger- und CC-Adressen (Rechnungsversand, Test-Mail, Kundenstammdaten) wurden nur
trim()t, aber nie auf gültiges Format oder enthaltene\r/\ngeprüft. DaSmtpMailerdas SMTP-Protokoll über einen rohen Socket implementiert, konnte eine Adresse mit eingebettetem CRLF zusätzlicheRCPT TO:-Kommandos oder Mail-Header einschleusen (CWE-93, SMTP-/Header-Injection). src/Services/SmtpMailer.php:send()validiert jetzt jede Empfängeradresse mitfilter_var(..., FILTER_VALIDATE_EMAIL), bevor überhaupt eine SMTP-Verbindung aufgebaut wird; ungültige Adressen (inkl. CRLF-Payloads) werfen eineRuntimeException. Da alle Versandwege (EmailService::sendInvoice(),sendTestMail()) durch diese einzige Methode laufen, ist die Prüfung zentral wirksam.src/Controllers/CustomerController.php: Formularvalidierung prüftemail,email_cc1,email_cc2zusätzlich mitfilter_var(FILTER_VALIDATE_EMAIL), damit gar nicht erst ungültige Adressen in der Kundendatenbank landen.src/Views/customers/form.php: Fehleranzeige für die drei E-Mail-Felder ergänzt.tests/50_EmailHeaderInjectionTest.php: Neuer Test prüft, dass CRLF-Injection-Payloads und ungültige Adressformate vonSmtpMailer::send()abgelehnt werden.
2026-07-08 – Fix: Unauthentifizierter Direktzugriff auf Belege/Rechnungs-PDFs (Issue #116)
- Kritisch: Hochgeladene Belege und Rechnungs-PDFs lagen unter
public/static/uploads/...im Web-Root und wurden von Apache direkt ausgeliefert, bevor der Requestindex.php/Auth::require()erreichte – ein Angreifer konnte Belege ohne Anmeldung per URL abrufen. src/Config.php:upload_dirzeigt jetzt aufdata/uploads(außerhalb des Web-Roots, analog zum bereits per Apache gesperrten/data-Verzeichnis)src/Models/Beleg.php: Neue MethodeabsolutePath()als einzige Quelle für die Pfad-Auflösung; unterstützt weiterhin Bestandsdaten mit dem alten Präfixstatic/uploads/instorage_pathsrc/Controllers/BelegController.php,InvoiceController.php,BankController.php,src/Services/NextcloudImportService.php,KontierungsvorschlagService.php: Alle Stellen, die Beleg-Dateien schreiben oder lesen, nutzen jetztConfig::get('upload_dir')bzw.Beleg::absolutePath()statt eigenerpublic/static/uploads-Pfadedocker-compose.yml,Dockerfile,docker-entrypoint.sh: Upload-Verzeichnis wird nach/var/www/html/data/uploadsgemountet/angelegt (bestehender Host-Ordner./uploadsbleibt unverändert)
2026-07-06 – Fix: Fehlermeldung beim Beleg-Löschen, falls Datei nicht löschbar ist (Issue #114)
src/Models/Beleg.php:delete()nutzte ein ungeschütztesunlink(); schlug dieses auf dem Server mit „Permission denied" fehl, wurde eine PHP-Warning ausgegeben, die den nachfolgenden Redirect-Header zerstörte („headers already sent"). Der Datensatz wurde dabei trotzdem gelöscht, was zu Verwirrung führte. Fehler beim Datei-Löschen wird jetzt abgefangen und nur noch geloggt (error_log), die Löschung des Datensatzes läuft unverändert durch.
2026-07-02 – Feature: Zusätzliche CC-E-Mail-Adressen pro Kunde (Issue #111)
migrations/084_customer_cc_emails.sql: Neue Spaltenemail_cc1,email_cc2incustomerssrc/Models/Customer.php:create()undupdate()speichern die beiden CC-Adressen (Whitespace wird zu NULL normalisiert)src/Views/customers/form.php: Zwei neue Eingabefelder „E-Mail CC 1/2" unter der Haupt-E-Mailsrc/Controllers/InvoiceController.php:show()lädt den Kunden und übergibt ihn an die Detailansichtsrc/Views/invoices/show.php: CC-Felder im E-Mail-Sende-Modal werden mit den hinterlegten Kunden-CC-Adressen vorbelegt
2026-06-29 – Feature: Ereignisse-Log für Nextcloud-Import (Issue #109)
migrations/083_system_events.sql: Neue Tabellesystem_events(id, channel, message, created_at)src/Models/EventLog.php: Neues Model mitadd()undfindByChannel()src/Services/NextcloudImportService.php: Persistiert relevante Ereignisse (Import, Duplikat, Fehler) automatisch; stille Läufe ohne Dateiaktivität erzeugen keine Einträge; Zusammenfassung wird nur bei tatsächlicher Aktivität geloggtsrc/Controllers/EventController.php: Neuer Controller für/ereignisseund/ereignisse/nextcloud-importsrc/Views/ereignisse/nextcloud_import.php: Log-Ansicht mit Zeitstempel, Kanal und farbiger Meldungsrc/Views/layout/nav.php: Neuer Menüpunkt „Ereignisse" unter Verwaltung mit aufklappbarem Unterpunkt „Nextcloud-Import"
2026-06-29 – Feature: Lieferant in Belegliste inline bearbeiten (Issue #108)
src/Views/belege/list.php: Lieferant-Spalte ist direkt in der Liste klickbar; zeigt ein Inline-Textfeld zur Eingabe, speichert mit Enter, bricht mit Escape ab; andere Spalten öffnen weiterhin die Vorschausrc/Models/Beleg.php: Neue MethodeupdateSupplier()aktualisiert nur diesupplier_idohne andere Metadaten zu verändernsrc/Controllers/BelegController.php: Neue MethodeupdateSupplier()als AJAX-Endpunktpublic/index.php: RoutePOST /belege/{id}/supplierregistriert
2026-06-11 – Fix: Undefined-Array-Key-Warning bei Buchungsvorlagen ohne Standard-Lieferant
src/Models/Buchungsvorlage.php:create()/update()prüften$data['standard_lieferant']direkt ohne Null-Coalescing, was bei fehlendem Schlüssel eine PHP-Warning auslöste
2026-06-11 – Feature: Liste verfügbarer Ollama-Modelle in den Einstellungen (Issue #103)
LlmService: Neue MethodegetAvailableModels()fragt/api/tagsab und liefert die Namen aller lokal verfügbaren Ollama-Modelle; neue GettergetMode(),getModel(),getVisionModel()- Einstellungsseite: Im KI-Abschnitt wird bei erreichbarem Ollama-Provider die Liste der verfügbaren Modelle angezeigt, das konfigurierte Standard- und Vision-Modell sind markiert
2026-06-11 – Feature: CSV-Importformat aus „Neue Direktverbindung"-Dialog anlegen (Issue #102)
bank/fints.php: Neue Auswahl „+ CSV-Importformat…" im Anbieter-Dropdown des „Neue Direktverbindung"-Dialogs; zeigt Felder für Trennzeichen, Anführungszeichen, Datumsformat, Dezimal-/Tausendertrennzeichen, Headerzeile und übersprungene ZeilenBankController::direktStore(): Behandeltprovider_id === '__csv__'gesondert – legt einenbank_sources-Eintrag mittype='csv'undprovider_id=NULLan (Standard-Spaltenzuordnung: Buchungstag=0, Wertstellung=1, Vorgang=2, Betrag=4) und leitet zur Feinabstimmung nach/settings/bankimportweiter
2026-06-11 – Fix: Direktabruf springt nach Abschluss zurück zu „Bank buchen" (Issue #101)
src/Views/bank/fints.php: Nach erfolgreichem Direktabruf (FinTS) führt der „Schließen"-Button im Abruf-Log-Modal jetzt zu einem Redirect auf/bank("Bank buchen"); bei Abbruch oder Fehler bleibt die Anzeige wie bisher auf/bank/direkt, damit der Abruf wiederholt werden kann
2026-06-08 – Fix: KI-Verbindungstest mit Diagnoseinformationen (Issue #55)
LlmService: Neue MethodetestConnection()gibt detaillierte Diagnose zurück (cURL-Fehlernummer/-meldung, HTTP-Code, Antwortzeit, verfügbare Modelle)LlmService:probeOllama()schreibt bei cURL-Fehlern jetzt einen Eintrag ins PHP-Fehlerlog (docker compose logs web)- Einstellungsseite: Neuer Button „Verbindung testen" im KI-Abschnitt; zeigt bei Erfolg verfügbare Modelle, bei Fehler cURL-Fehler, URL und Firewall-Hinweis an
- Route:
POST /settings/llm-test→SettingsController::llmTest() BelegController: Bei Verbindungsfehler wird jetzt[]stattnullgespeichert (verhindert endlose Auto-Retry-Schleife); Fehlermeldung differenziert zwischen „Ollama nicht erreichbar" und „kein Vorschlag generiert"- Beleg-Detailseite: Zeigt bei Verbindungsfehler spezifische Meldung „Ollama nicht erreichbar" mit Link zu den Einstellungen statt der generischen Warnung
2026-06-01 – Fix: EN16931-Validierungsfehler in ZUGFeRD-Rechnungen
- [BR-FX-EN-04] behoben:
ZugferdServicesetzt jetzt BT-72 (Liefer-/Leistungsdatum) viasetDocumentSupplyChainEvent(); das Datum wird aus dem Feldleistungszeitraumgeparst (FormatDD.MM.YYYY→ direkt; FormatMM.YYYY→ letzter Tag des Monats; sonst Fallback auf Rechnungsdatum) - [PEPPOL-EN16931-R008] behoben: Leere Strings in optionalen Adressfeldern (Straße, PLZ, Ort) werden als
nullübergeben, sodass keine leeren XML-Elemente entstehen
2026-05-28 – Feature: Buchungsvorlagen im Buchungsdialog
- Datenbank: 3 neue Tabellen
buchungsvorlagen,buchungsvorlage_felder,buchungsvorlage_regeln(Migrationen 078–080); initiale Vorlage "Essensbeleg n. abzugsfähig (nur USt.)" per Migration 080 eingespielt - Buchungsdialog (
/belege/{id}): Neues Vorlagen-Badge (gelb) zwischen Bank-Badge und Buchungsformular; Auswahlbox mit aktiven Vorlagen; Vorlage auswählen öffnet modalen Dialog mit Felder-Gruppen und Live-USt-Berechnung - Modal: Feldgruppe 1 (Metadaten: Lieferant, Rechnungsnr., Belegdatum, Brutto-Anzeige), Feldgruppe 2 (USt-Zeilen mit Netto↔Brutto-Live-Berechnung), Feldgruppe 3 (Zahlungskonto; 1200 vorausgewählt wenn Bankbuchung, sonst 1210)
- Buchungslogik:
VorlageBuchungsServiceinterpretiert JSON-Buchungsregeln und ruftBooking::create()direkt auf (keine automatischen Kindzeilen, da Konten explizit); Metafelder werden überBeleg::updateMeta()gespeichert - Pflege-UI:
/einstellungen/buchungsvorlagen– CRUD-Oberfläche für Vorlagen, Felder und Buchungsregeln; Navigationslink unter Einstellungen - Test 42:
42_BuchungsvorlageTest.phpprüft CRUD, Buchungsausführung, korrekte Konten undneq_zero-Bedingung
2026-05-24 – Fix: EÜR-Zeilennummern auf amtliche Anlage EÜR 2025 korrigiert
migrations/075_euer_zeilen_korrektur.sql: alleeuer_zeile-Werte inaccountsgemäß amtlicher Anlage EÜR 2025 neu zugeordnet (zuvor internes Schema ohne Bezug zum amtlichen Formular)EuerCalculator.php: Sonderbehandlung für nicht abziehbare BA voneuer_zeile='51'auf'74'umgestellt (Zeile 74 = „Nicht abziehbare Beträge" der amtlichen Anlage EÜR)accounts/form.php:$euerZeilen-Array vollständig durch amtliche Zeilen 12–72 und 74 ersetzt; alle Beschriftungen entsprechen exakt dem Formulartext der Anlage EÜR 2025
2026-05-24 – Feature: EÜR Anlageliste CSV/PDF-Export
- EÜR-Ansicht: Kopfzeile der Anlageliste-Karte enthält jetzt rechtsbündig „Herunterladen: [CSV] [PDF]"
- CSV-Export:
GET /tax/euer/{year}/anlageliste/csv– alle 9 Spalten mit UTF-8 BOM (Excel-kompatibel), Semikolon-getrennt, inkl. Summenzeile - PDF-Export:
GET /tax/euer/{year}/anlageliste/pdf– A4 Querformat, enge Spaltenbreiten sodass alle 9 Spalten auf eine Seite passen; neuerPdfAnlagelisteService - EÜR-Karte: heller „PDF herunterladen"-Button entfernt; Topbar-Button umbenannt zu „PDF"
2026-05-24 – Feature: AFA-Buchung stornieren/löschen in AfA-Tabelle
- AfA-Tabelle im Asset-Edit-Modal: Gebuchte Zeilen zeigen neben dem grünen Haken einen Storno-Button (Undo-Pfeil, alle Modi) und im Testmodus zusätzlich einen Papierkorb-Button
- Storno legt eine Gegenbuchung (
buchungstyp=AfA,is_reversal=1, negativer Betrag) an und setztafa_booking_idauf NULL → der Pfeil-aus-Box-Button erscheint wieder - Löschen (Testmodus) löscht die AFA-Buchung direkt und setzt
afa_booking_idauf NULL - Neuer Controller:
POST /assets/{id}/depreciation/{rowId}/reverseund…/delete AssetDepreciation::setBookingId()akzeptiert jetzt?int(null erlaubt)
2026-05-24 – Feature: Buchungsjournal-Filter Buchungstyp + EÜR-Anlageliste
- Buchungsjournal: neues Filterfeld „Buchungstyp" (Einnahme / Ausgabe / AfA); vorbelegt mit „Alle Typen"
- Buchungsjournal: AfA-Buchungen (ohne Beleg) werden jetzt korrekt angezeigt (
LEFT JOIN belege); Beleg-Zelle zeigt „–" wenn kein Beleg - Buchungsjournal: Badge für Buchungstyp
AfAin grau (badge-gray) - EÜR-Ansicht: neue Tabelle „Anlagegüter {Jahr}" unterhalb der EÜR mit Spalten Anlage-ID, Bezeichnung, Anschaffungsdatum, Anschaffungskosten, Nutzungsdauer, AfA Vorjahre (kumulierte gebuchte AfA-Buchungen), Restbuchwert Vorjahr, AfA {Jahr} (gebuchte AfA des Jahres), Restbuchwert 31.12.{Jahr}; Summenzeile am Ende
Asset::anlageliste(int $year)als neue Modell-Methode (single query mit Subqueries)
2026-05-24 – Feature: AfA-Tabelle anteilig nach Anschaffungsmonat
- Das erste Abschreibungsjahr wird anteilig nach Restmonaten berechnet: der Anschaffungsmonat zählt voll (Januar = 12 Monate, April = 9 Monate usw.)
- Das letzte Jahr erhält automatisch die verbleibenden Monate (12 − Monate des ersten Jahres); bei Januar-Kauf entfällt das extra letzte Jahr
- Die Cent-Option (
cent_im_ersten_jahr) bleibt davon unberührt: der Cent-Rest geht weiterhin vollständig ins erste Jahr, ohne Aufteilung auf das letzte Jahr - Sofortabschreibung (
nutzungsdauer = 0) ist unverändert - Neuer Test
41_AfaAnteiligTestprüft April-, Oktober-, Juli-, Dezember- und Januar-Käufe sowie die Cent-Option mit anteiligem Jahr
2026-05-22 – Feature: „Abrufen ab"-Auswahl für Direktverbindungen
- Im Bearbeiten-Dialog jeder FinTS- bzw. API-Direktverbindung gibt es ein neues Dropdown Abrufen ab mit den Optionen Ab letztem Abruf (00:00 Uhr) (Default), Letzte 7 Tage, Aktueller Monat (ab 01.), Letzte 30 Tage, Ohne Datum (letzte 365 Tage)
- Die Auswahl gilt nur für den nächsten Abruf: nach erfolgreichem Lauf setzt
BankSource::updateLastFetched()fetch_rangewieder auf'last_fetched'zurück - Migration
072_bank_sources_fetch_range.sqlergänzt die neue ENUM-Spalte (Default'last_fetched', bestehende Verbindungen verhalten sich unverändert) - Logik in
BankSource::computeStartDate()zentralisiert;FinTsFetchServiceundPayPalNvpFetchServicenutzen sie. Neuer Test39_BankFetchRangeTestdeckt alle fünf Range-Werte plus Fallbacks ab
2026-05-22 – Fix: FinTS-Konto-Auswahl wird jetzt gespeichert + Bearbeiten-Dialog mit Checkboxen
- Bug: Beim ersten Abruf erschien das Konto-Auswahl-Modal, das Speichern blieb aber hängen. Ursache war PHP-Session-Locking: der SSE-Stream
/bank/direkt/{id}/runhielt die Session-Datei während desawaitAccountSelection()-Polling-Loops gesperrt, der parallele POST/bank/direkt/{id}/accountsblockierte beimsession_start()→ Deadlock bis Timeout - Fix: Im SSE-Endpoint
session_write_close()direkt nach dem Auth-Check; entstört zugleich die analog gelagerte TAN-Eingabe - UI: Im Bearbeiten-Dialog einer FinTS-Verbindung erscheinen die gespeicherten Konten jetzt als Checkbox-Liste (statt nur als Text). Abhaken entfernt den Identifier beim Speichern aus
account_filter; alle abgehakt verhält sich wie der Reset-Toggle - Filter-Berechnung als
BankSource::computeFilterFromPost()extrahiert, neuer Test38_BankSourceFilterFromPostTestdeckt Reset, Teilauswahl, leere Auswahl und Schutz gegen Unterschieben fremder Identifier ab
2026-05-22 – Feature: APP_KEY Status & Generator in den Einstellungen
- Neuer Settings-Bereich unter dem Betriebsmodus zeigt Status des Verschlüsselungsschlüssels (
APP_KEY): Gesetzt (grün, 64 Hex-Zeichen valide), Ungültig (falsches Format) oder Fehlt (leer) - Bei fehlendem oder ungültigem Schlüssel: Button „Neuen Schlüssel generieren" → POST
/settings/generate-app-keyliefertbin2hex(random_bytes(32)), Anzeige im Code-Block mit Copy-Button - Schlüssel wird nicht persistiert (Container hat keinen Schreibzugriff auf
.env); Anleitung im UI: kopieren → in.enveintragen →docker compose restart web
2026-05-22 – UI: Einheitliche Icon-Buttons für Bearbeiten/Ansehen/Löschen
- Bleistift-Icon (transparent, ohne Hintergrund – Stil der Direktverbindungs-Kacheln) ersetzt alle „Bearbeiten"- und „Öffnen"-Buttons in
accounts/list,customers/show,customers/list,suppliers/list,useradmin/list,assets/list,belege/list,invoices/list,invoices/show - Auge-Icon (Kästchen mit Auge) ersetzt die PDF-Buttons in
invoices/list(PDF),invoices/show(PDF herunterladen) sowie den „Ansehen"-Button intax/ustva_index, um „nur ansehen" visuell vom „bearbeiten" abzugrenzen - Papierkorb-Icon (transparent, Danger-Farbe) ersetzt alle „Löschen"-Buttons in
accounts/list,invoices/list,useradmin/list,suppliers/list,assets/list,belege/detail,settings/bankimport,bank/fintsundsettings/form - Plus-Icon (rund, btn-primary, 1,8rem) ersetzt alle „Neu anlegen/erstellen/hochladen"-Buttons – Topbars (
bank/fints, Kunden, Lieferanten, Rechnungen, Belege, Wirtschaftsgüter, Konten), Dashboard-Schnellzugriff, Card-Header incustomers/show, Aktionsspalte incustomers/listund „Position hinzufügen" im Rechnungsformular bank/index.php: Button „Öffnen" → „Aktivieren" (Status-Toggle, semantisch eindeutiger)
2026-05-21 – Feature: FinTS-Direktverbindung + Vereinheitlichung der Bank-Importquellen
- FinTS-Abruf für Comdirect, DKB und weitere Banken über
nemiah/php-fints(PHP-Lib für HBCI/FinTS 3.0). Leseabruf mit PIN; TAN-pflichtige Aktionen werden mit klarer Fehlermeldung abgebrochen - Verschlüsselung sensibler Felder: Neuer
Crypto-Service (AES-256-GCM, Magic-Prefixenc:v1:) verschlüsselt PayPal-Signature und FinTS-PIN. Schlüssel ausAPP_KEYin.env– im Backup mitsichern, Verlust = Verlust aller verschlüsselten Werte - Datenmodell-Fusion: Neue Tabelle
bank_sourcesersetztbank_import_formats+bank_connectionsmittype-Spalte (csv/api/fints).bank_import_batches.source_idlöstformat_id/connection_idab (alte Spalten bleiben vorerst bestehen für rollback-Sicherheit) - Route-Umbenennung:
/bank/fints*→/bank/direkt*(semantisch korrekt – PayPal ist keine FinTS-Verbindung). Topbar in /bank vereinheitlicht zu einer „Importieren"-Schaltfläche - NEU/Edit-Modal zeigt typabhängig API-Felder (PayPal: App-Name, Kennwort, Signature, Sandbox) oder FinTS-Felder (BLZ, Benutzerkennung, PIN, optional FinTS-URL und IBAN-Filter)
FinTsFetchService: Login → SEPA-Konten abrufen → Statements seitlast_fetched_at(00:00) → Buchungen mit Hash-basierterexternal_id(IBAN+Datum+Betrag+Verwendungszweck) deduplizieren- Migrationen 068 (bank_sources + Backfill), 069 (default_fints_url auf bank_providers für Comdirect/DKB)
scripts/encrypt_bank_secrets.php– einmaliges idempotentes Verschlüsseln der Klartext-Werte nach Backfill- Neue Tests 36 (Crypto-Roundtrip) und 37 (BankSource CRUD über alle drei Typen)
2026-05-17 – Feature: Automatischer Format-Vorschlag beim Bank-Import
- Format-Vorschlag: Wenn eine hochgeladene CSV keinem bekannten Format eindeutig entspricht, analysiert
BankImportParser::suggestFormat()die Datei automatisch (Headerzeilen-Erkennung bei skip_rows 0–10, Delimiter-Erkennung, Keyword-basiertes Spalten-Mapping) - Session-gestützter Vorschlags-Flow: Klick auf „Formate bearbeiten" speichert den Vorschlag via AJAX in der Session und öffnet die Einstellungsseite
- Vorschlags-Card: Auf
/settings/bankimporterscheint der Vorschlag oberhalb bestehender Formate als ausfüllbares Formular mit vorausgefüllten Feldern (skip_rows, Delimiter, Spalten-Mapping mit erkannten Headernamen) - Explizites Speichern: Neues Format wird erst nach Klick auf „Speichern" in der DB angelegt (POST
/settings/bankimport/create); Verwerfen löscht die Session-Suggestion - Neue Routen:
POST /settings/bankimport/suggest,POST /settings/bankimport/create,POST /settings/bankimport/dismiss-suggestion - Neues Model:
BankImportFormat::create(); neuer Test 35
2026-05-17 – Feature: Automatische Format-Erkennung beim Bank-Import
- Format-Erkennung: Hochgeladene Datei wird automatisch gegen alle gespeicherten Formate geprüft; bei eindeutigem Match wird sofort importiert
- Vorschau-Panel: Wenn kein Format passt, zeigt die Upload-Seite Überschriften-Zeile, erste Werte-Zeile und die Spalten-Zuordnung (Wertstellung, Buchungstext, Wert) für das gewählte Format an; wechselt live beim Umschalten der Formatauswahl
- „Formate bearbeiten"-Button oben rechts verlinkt direkt auf die Einstellungsseite für Bankimport-Formate
- Neuer AJAX-Endpunkt
POST /bank/detect-formatinBankController; neue MethodenpeekRows()undisFormatMatch()inBankImportParser
2026-05-17 – Feature: DKB-Importformat für Bankbuchungen
- Neues Format „DKB Bank": 4 Skip-Zeilen, Semikolon-getrennt, Anführungszeichen-eingeschlossen
- Buchungstext wird konditionell zusammengesetzt: bei negativem Betrag aus Zahlungsempfänger + Verwendungszweck, sonst aus Zahlungspflichtiger + Verwendungszweck
- Migration 054 ergänzt drei neue Nullable-Spalten in
bank_import_formats(col_zahlungspflichtiger,col_zahlungsempfaenger,col_verwendungszweck)
2026-05-16 – Refactor: Code-Qualität, Sicherheit und Konsistenz
- CSRF-Schutz:
Auth::generateCsrfToken()/validateCsrfToken()neu; Router validiert Token bei allen POST/PUT/DELETE-Requests; base.php injiziert Token in Formulare und fetch-Aufrufe via JS; login.php erhält hidden Input - Path Traversal in
BelegController::serveFile()abgesichert (realpath()+ Prefix-Check gegen Upload-Basisverzeichnis) mime_content_type()(deprecated) durchfinfo_file(finfo_open(FILEINFO_MIME_TYPE), ...)inBelegController,SettingsController,BankControllerundImageCropServiceersetzt- XSS in
topbarActions:View::buildQuery()(neuer Helper) statt rohemhttp_build_query($_GET) @-Unterdrückung inSettingsControllerentfernt (ersetzt durchfile_exists()-Prüfung)- BaseController: gemeinsame
renderView()-Methode zentralisiert; alle 13 Controller erben davon; lokale Duplikate entfernt - DB-Transaktionen:
Booking::create()undBooking::reverse()laufen jetzt in atomaren Transaktionen;Database::beginTransaction/commit/rollBackunterstützt verschachtelte Aufrufe via Tiefenzähler (TestCase-Isolation bleibt intakt) src/Constants.php: Buchungstypen und Steuerkontonummern (SKR03) als benannte Konstanten; Magic Strings inBooking.phpersetzt- Dokumentation:
docs/funktionen.md– Abschnitt „Bank buchen" ergänzt; UStVA-Kennzahlen (81, 89, 66, 61, 83) korrigiert; Eigenanteil-Konten (4920/4925) korrigiert - Tests:
32_BankTransactionTest(linkBeleg, unlinkBeleg, recalcStatus, findDuplicates) und33_BankImportParserTest(CSV-Parsing, BOM, Tausendertrenner, Fehlerfall) neu
2026-05-15 – Feature: Bankbuchung auf mehrere Belege aufteilen
- Migration 053: Junction-Tabelle
bank_transaction_belege(bank_tx_id, beleg_id, betrag);bank_transactions.beleg_identfernt; Status-ENUM umteilweiseerweitert; bestehende Verknüpfungen automatisch migriert - Neuer Status
teilweise: wird gesetzt wenn mind. ein Beleg verknüpft ist, aber noch Restbetrag offen bleibt - Standardfilter in der Bankübersicht zeigt jetzt „Offen & Teilweise"
bank/show.php: Sektion „Verknüpfte Belege" mit Betrag je Beleg und Entfernen-Button; Restsaldo-Anzeige; Betrag-Eingabefeld pro Beleg in der Offene-Belege-Liste- Beim Verknüpfen wird der zugeordnete Betrag an die Buchungsmaske weitergegeben (
alloc_betrag) BankTransaction::linkBeleg()schreibt jetzt in Junction-Tabelle mit Betrag;recalcStatus()setzt Status automatisch;unlinkBeleg()entfernt Verknüpfung;getLinkedBelege()/getRemainingBetrag()neu- Neue Route
POST /bank/{id}/unlink
2026-05-15 – Feature: Dublettenprüfung beim Bank-Import
- Nach dem Upload wird geprüft, welche Buchungen bereits in der DB vorhanden sind (Abgleich über Buchungstag, Wertstellung, Betrag, Buchungstext)
- Werden Dubletten gefunden, erscheint eine Prüfseite mit zwei Tabellen: erkannte Dubletten (per Checkbox auswählbar) und neue Buchungen (werden immer importiert)
- „Alle auswählen / abwählen"-Buttons + Gesamtzähler zeigen an, wie viele Buchungen importiert werden
- Erst nach Bestätigung wird importiert; ohne Auswahl wird der Import übersprungen
- Migration 052: UNIQUE-Constraint auf
bank_transactionsentfernt (UI-Prüfung ist die primäre Schutzschicht) BankTransaction::findDuplicates(): neue Methode mit Batch-Abfrage gegen die DBBankController::doImport(): gemeinsame Import-Logik extrahiert (wird vonupload()unduploadConfirm()genutzt)
2026-05-15 – Fix: Bank-Import-Duplikate verhindern
- Migration 051: UNIQUE-Constraint
(buchungstag, wertstellung, wert, buchungstext(200))aufbank_transactions; bestehende Duplikate wurden dabei bereinigt BankController::upload(): Import läuft jetzt in einer DB-Transaktion – bei Fehler wird alles zurückgerollt statt einen unvollständigen Batch zu hinterlassenBankController::upload(): Dateiname-Prüfung vor dem Import – erneuter Upload derselben Datei wird mit Fehlermeldung abgelehntBankTransaction::filenameExists(): neue Hilfsmethode zur Duplikat-Prüfung anhand Dateiname
2026-05-15 – Fix: Netto-Neuberechnung bei Kategorie-Wechsel in Bank-Buchungsmaske
- IIFE-Wrapper für
updateAutoUstin einenDOMContentLoaded-Listener verschoben, damit er erst nach der Funktionsdefinition (späteres<script>-Tag) gesetzt wird und nicht sofort von ihr überschrieben wird.
2026-05-14 – Feature: Bank buchen
- Navigation: neuer Eintrag „Bank buchen" unter Belege & Buchungen
- Migration 049: Tabellen
bank_import_formats,bank_import_batches,bank_transactions; Comdirect-Format als erster Eintrag BankImportParser: Parst CSV/TXT (quoted, Semikolon-getrennt) und XLSX (via ZipArchive/SimpleXML, kein Composer-Paket)- Liste
/bank: Suche + Filter (Jahr, Monat, Einnahme/Ausgabe, Status); Standard-Filter: offen; Einnahmen grün, Ausgaben rot - Upload
/bank/upload: Drag-&-Drop-Zone für CSV/TXT/XLSX + Format-Auswahl - Detail
/bank/{id}: Bank-Zeile oben; Beleg-Upload; Liste offener Belege zum Verknüpfen - Status-Aktionen: „Ignorieren" / „Wieder öffnen"; Verknüpfen eines Belegs setzt Status auf „gebucht"
2026-05-14 – Feature: UStVA Kz 83 = ust81 + ust89 − Kz66 − Kz61
UstvaCalculator: Kz 83 wird nun alsust81 + ust89 − Kz66 − Kz61berechnet (nicht mehr kontobasiert via ustva_kz='83')- Test 29: angepasst (7%-Buchung auf korrektes Konto 8300; delta83-Erwartung aktualisiert)
2026-05-14 – Feature: UStVA Kz 61 aus ust89 abgeleitet (ReverseCharge-Symmetrie)
UstvaCalculator: Kz 61 = ust89 (nicht mehr kontobasiert via Konto 1574) – fachlich korrekt, da VSt ig. Erwerb = USt ig. ErwerbBooking::reverse(): Hauptstornobuchung überträgt jetztsteuerartder Originalbuchung (fehlte bisher, wurde durch Kz-61-Kontoberechnung kaschiert)
2026-05-14 – Feature: UStVA Kz 81/89 auf volle Euro abgerundet
UstvaCalculator: Kz 81 und 89 werden mitfloor()auf volle Euro abgerundet; USt wird auf dem gerundeten Wert berechnet- Darstellung: Kz 81/89 zeigen "1.234,- €" statt "1.234,00 €" (in Tabelle und Zusammenfassung)
- USt-Felder (ust81, ust89) bleiben als Dezimalzahl unverändert
- Test 31: prüft floor-Verhalten für Kz 81 und 89
2026-05-14 – Refactor: UStVA auf 5-Kennzahlen-Schema vereinfacht
- Migration 048: Kontenrahmen bereinigt – nur noch
ustva_kz∈ {81, 83, 61, 66} aktiv; 1571/1576 → 66, 1574 → 61, 1771/1774 → 83, alle anderen auf NULL UstvaCalculator: Nur noch Kz 81, 89, 66, 61, 83 + zwei Anzeigefelder (ust81, ust89 = Kz × 0,19, kein XML)- Kz 83 ist jetzt „Vorauszahlung / Überschuss" =
(USt-Konten) − Kz 66 − Kz 61 ElsterXmlService: Export nur Kz 81/89 (ganzzahlig) + 66/61/83 (2 Dez); Kz 83 nur bei positivem Wert- View
ustva_show.php: Neue Sektionsgliederung, Zusammenfassungsbox aktualisiert - Tests 5, 7, 13, 18, 28, 29, 30 auf neues Schema angepasst
2026-05-10 – Feature: ig. Erwerb 19 % ReverseCharge über Kindzeilen
- Migration 047: ustva_kz='91' für Konto 1774; Kindzeilen (1574/1774) für bestehende ig_erwerb_19-Buchungen
Booking::create(): steuerart=ig_erwerb_19 erzeugt zwei Kindzeilen – VSt auf 1574 (Kz 67), USt auf 1774 (Kz 91)UstvaCalculator: Kz 67/91 jetzt kontobasiert via 1574/1774; Kz 93/94 (ig_erwerb_7) entfernt- Buchungsformular: Option „ig. Erwerb 7 %" entfernt
- Test 7 auf neue Logik angepasst; Test 30 neu
2026-05-10 – Feature: USt-Buchungszeilen für Ausgangsrechnungen
- Migration 046: Konten 1771 (USt 7 %) und 1776 (USt 19 %) erhalten
ustva_kz='84'/'83'`; bestehende Einnahme-Buchungen mit USt erhalten nachträglich USt-Kindzeilen Booking::create(): Bei Einnahme-Buchungen mit 7/19 % undsteuerart=normalwird analog zur VSt-Kindzeile eine USt-Zeile auf 1771/1776 angelegtUstvaCalculator: Kz 83/84 jetzt kontobasiert via 1771/1776 (betrag_netto), nicht mehr viabetrag_ustder Erlöskonten;KZ_ACCOUNT_MAPaktualisiertInvoice::cancel(): Nur Hauptbuchungen (parent_booking_id IS NULL) werden direkt storniert; Kindzeilen werden vonBooking::reverse()mitaufgehoben (verhindert Doppelstorno)- Tests 10 und 27 an geänderte Buchungsstruktur angepasst; Test 29 neu
2026-05-05 – Feature: Stornorechnung (§ 14 UStG)
Hinzugefügt
migrations/042_invoice_storno_of_id.sql: Neue Spaltestorno_of_idininvoices(FK auf Original)Invoice::createCancellation(): Legt Stornorechnung mit negierten Beträgen an, setzt Original auf „Storniert", erzeugt GoBD-konforme Stornobuchungen- Storno-Button in Rechnungsübersicht (
list.php) und Detailansicht (show.php) - Querverweise Original ↔ Stornorechnung in der Detailansicht
- PDF-Anpassung: Titel „STORNORECHNUNG", Referenzzeile, kein Zahlungshinweis, angepasster Dateiname
- Test
27_StornorechnungTest.php
2026-04-28 – Feature: E-Rechnung / ZUGFeRD EN16931
Hinzugefügt
src/Services/ZugferdService.php: Erzeugt ZUGFeRD-konformes EN16931-XML und bettet es in den PDF-String ein (viahorstoeko/zugferd)src/Services/PdfInvoiceService.php: PDF wird nach der mPDF-Generierung durchZugferdService::attachToPdf()geleitet; Ausgabe erfolgt jetzt manuell per Header/echo stattmpdf->Output('D')- Alle Ausgangsrechnungs-PDFs sind ab sofort gültige E-Rechnungen (ZUGFeRD EN16931 / Factur-X)
2026-04-27 – Feature: Neues Rechnungsnummernformat (PPPYYYYMM-nn)
Geändert
src/Models/Invoice.php:generateRechnungsNr()erzeugt jetzt das FormatPPPYYYYMM-nn(Prefix + Jahr + Monat + zweistellige Sequenz, die bei Bedarf wächst); Zähler wird monatlich zurückgesetztmigrations/038_invoice_nr_month.sql: Neue Spalteinvoice_nr_monthincompany_settings
2026-04-26 – Fix: Eigenanteil-Buchung als Einnahme statt Ausgabe
Geändert
src/Models/Booking.php:upsertKorrekturbuchung()setztbuchungstypjetzt korrekt aufEinnahme(war fälschlichAusgabe)
2026-04-26 – Feature #57: Lineare Abschreibungstabelle für Anlagegüter
Hinzugefügt
migrations/035_assets_abschreibung_flags.sql: Neue Spaltenautomatisch_berechnetundcent_im_ersten_jahrinassetsmigrations/036_asset_depreciation_schedule.sql: Neue Tabelleasset_depreciation_schedule(Jahr, Abschreibungsbetrag, Restwert)src/Models/AssetDepreciation.php: Berechnung (linear, Sofortabschreibung, Cent-Option), Persistenztests/26_AbschreibungstabelleTest.php: 22 Tests für alle AfA-Szenarien
Geändert
src/Models/Asset.php:create()undupdate()speichernautomatisch_berechnetundcent_im_ersten_jahrsrc/Controllers/AssetController.php: Abschreibungstabelle wird nach Speichern automatisch generiert; neue Endpunkte/assets/depreciation-preview(AJAX-Vorschau) und/assets/{id}/depreciation(manuelle Speicherung)src/Views/assets/form.php: Nutzungsdauer-Dropdown (sofort, 1–10, 12, 13, 15, 20, 25, 50 Jahre + individuell), Cent-Option, „AfA-Tabelle"-Button, Modal mit Auto/Manuell-Toggle und editierbarer Tabellepublic/index.php: Neue Routen für Depreciation-Preview und -Speicherung
2026-04-26 – KI-Diagnose: Erreichbarkeitsprüfung und Fehlerlog für Ollama
Geändert
src/Services/LlmService.php: Neue MethodeisReachable()für Diagnosezwecke; Fehlerlog incallOllama()bei cURL-Fehler oder leerer Antwortsrc/Views/settings/form.php: Einstellungsseite prüft jetzt aktiv, ob der Ollama-Server erreichbar ist (nicht nur ob er konfiguriert ist); zeigt bei Verbindungsfehler einen konkreten Hinweis aufOLLAMA_HOST=0.0.0.0:11434
2026-04-26 – Leistungszeitraum als freies Textfeld
Geändert
migrations/034_leistungszeitraum_text.sql: Spaltenleistungszeitraum_von(DATE) undleistungszeitraum_bis(DATE) wurden durch eine einzige Textspalteleistungszeitraum(VARCHAR 255) ersetzt; bestehende Datumswerte werden automatisch migriertsrc/Views/invoices/form.php: Zwei Datumsfelder durch ein Freitextfeld „Leistungszeitraum" ersetzt, Grid von 4 auf 3 Spalten angepasstsrc/Models/Invoice.php: INSERT/UPDATE verwenden jetztleistungszeitraumstattleistungszeitraum_von/leistungszeitraum_bissrc/Controllers/InvoiceController.php: Datums-Defaults fürleistungszeitraum_von/leistungszeitraum_bisentferntsrc/Views/invoices/show.php: Leistungszeitraum-Anzeige vereinfacht (kein Datumsformat, direkter Textausgabe)src/Services/PdfInvoiceService.php: Leistungszeitraum-Ausgabe vereinfacht
2026-04-15 – KI-Erkennung: automatischer Aufruf aus Belegansicht entfernt
Geändert
src/Views/belege/detail.php: KI-Analyse wird nicht mehr automatisch beim Öffnen eines Belegs ausgelöst; stattdessen wird ein Hinweis "KI-Analyse wird im Hintergrund durchgeführt …" mit einem "Jetzt generieren"-Button angezeigt, wenn noch kein Vorschlag vorhanden ist- Hintergrund: Die Analyse läuft bereits beim Nextcloud-Import (
ki-batch.php); ein doppelter Aufruf aus der UI war überflüssig
2026-04-12 – Anlagevermögen: Anzeige-Fix und Daten-Backfill
Fix
- Buchungstabellen (Beleg-Detail, Journal, UStVA-Drill-down, EÜR-Drill-down): Anlage-Spalte zeigt jetzt auch die Bezeichnung als Fallback, wenn
anlage_nrleer ist migrations/032_anlage_nr_backfill.sql: Anlage-Nummern für bestehende Anlagegüter ohneanlage_nrwerden nachgeneriert
2026-04-12 – Anlagevermögen (Wirtschaftsgüter)
Neu
- Navigation: neuer Abschnitt „Betriebsvermögen" mit Menüpunkt „Anlagevermögen"
migrations/028_assets_anlagevermoegen.sql: neue Tabelleassetsfür Wirtschaftsgütersrc/Models/Asset.php: CRUD-Model mit automatischer laufender Nummer und Geschäftsjahrzuordnungsrc/Controllers/AssetController.php: Liste, Anlegen, Bearbeiten, Löschensrc/Views/assets/list.php: Anlagenspiegel-Tabelle mit Bleistift- und Papierkorb-Aktionensrc/Views/assets/form.php: Formular für Wirtschaftsgut (Bezeichnung, Anschaffungsdatum, Kosten, Zugänge, Abgänge, AfA, Nutzungsdauer, Notizen)tests/20_AnlagevermoegensTest.php: 14 Tests für CRUD-Operationen
2026-04-12 – EÜR-Korrekturen: Gewerbesteuer + Bewirtungskonten ohne VSt
Geändert
migrations/028_euer_4320_bewirtung_ohne_vst.sql: Gewerbesteuer (4320) von Z. 52 auf Z. 51 (§ 4 Abs. 5b EStG, KZ 165 nicht abzugsfähig)migrations/028_euer_4320_bewirtung_ohne_vst.sql: Konten 4652 (Bewirtung 70 % ohne VSt, Z. 44) und 4657 (Bewirtung 30 % ohne VSt, Z. 51) ergänztdocs/kontenrahmen.md: Dokumentation entsprechend aktualisiert
2026-04-12 – Klasse-0-Konten als Anlagevermögen klassifiziert
Geändert
docs/kontenrahmen.md: Alle 10 Klasse-0-Konten (0027–0480) vonZahlungskontoaufAnlagevermögenkorrigiertmigrations/027_anlagevermögen_kontenart.sql: Migration war bereits vorhanden und setzt die DB-Änderung beim nächsten Container-Start um
2026-04-12 – Backup: smarte Rotation und Lock-Schutz
Geändert
scripts/backup.sh: Rotation auf 7 Tages- / 4 Wochen- / 12 Monats-Buckets umgestellt (statt pauschaler 90-Tage-Löschung)scripts/backup.sh:flock-Lock verhindert parallele Läufe bei Verzögerungenscripts/backup.sh: Pfade sind absolut (Script funktioniert auch aus Cron ohne Verzeichniswechsel)
2026-04-12 – Uploads: named Volume → Bind Mount
Geändert
docker-compose.yml: Upload-Volume von named Volume (steuerkram_uploads) auf Bind Mount (./uploads/) umgestellt – konsistent mit./data/mariadb/scripts/migrate-uploads-to-bindmount.sh: Einmalige Migration hinzugefügt
Update auf Produktiv-System
Vor docker compose up einmalig ausführen:
./scripts/migrate-uploads-to-bindmount.sh
Das Script kopiert alle Dateien aus dem named Volume in ./uploads/, ist idempotent und gibt am Ende den Befehl zum Löschen des alten Volumes aus.
2026-04-11 – Buchungsbasierte Vorsteuer Kz 61/62 in UStVA
Geändert
UstvaCalculator: Kz 61 und 62 werden nicht mehr überaccounts.ustva_kzgeroutet, sondern direkt ausbookings.ust_satzermittelt (buchungstyp='Ausgabe',betrag_ust != 0). Damit reicht ein einziges Aufwandskonto für beide USt-Sätze – separate VSt19-/VSt7-Konten sind nicht mehr erforderlichfetchBookingsForKz: Drill-down für Kz 61/62 nutzt jetzt ebenfalls buchungsbasierte FilterungBERICHTE.md: Routing-Logik für Kz 61/62 dokumentiert
2026-04-10 – Nextcloud-Belegimport via WebDAV
Neu
NextcloudImportService: importiert Belege aus einem Nextcloud-Ordner via WebDAV (PROPFIND → GET → MOVE/DELETE); Duplikat-Erkennung via SHA-256; JPEG/PNG werden nach dem Download automatisch zugeschnittenscripts/nextcloud-import.php: CLI-Einstiegspunkt für Cron-Betrieb- Migration 023: Nextcloud-Zugangsdaten (
nextcloud_url,nextcloud_user,nextcloud_pass,nextcloud_folder,nextcloud_done) incompany_settings; Konfiguration über Einstellungen → Nextcloud-Belegimport in der UI
2026-04-10 – UStVA Vorsteuer-Kz-Aufteilung (Migration 022)
Fix
- Migration 022: Vorsteuer-Konten von
ustva_kz='66'auf'61'(VSt19) bzw.'62'(VSt7) aufgeteilt – Kz 61, 62 und 66 wurden zuvor stets als 0 ausgewiesen UstvaCalculator: zweite Abfrage für steuerart-basierte EU-Kennzahlen (ig. Lieferung → Kz 41, Ausfuhr → Kz 44, ig. Erwerb → Kz 89/91/93/94); verhindert Doppelzählung gegenüber kontobasierter BerechnungfetchBookingsForKz: UNION-Abfrage aus Konto-Kz und steuerart für korrekten Drill-down- Tests 4, 5, 6, 9: Kontonummern auf tatsächliche SKR03-Nummern korrigiert (8000→8400, 1400→1200)
2026-04-10 – Upload-Limit auf 20 MB erhöht
Geändert
php/uploads.ini:upload_max_filesizeundpost_max_sizeauf 20 MB gesetzt (zuvor PHP-Standard 2 MB)Dockerfile: bindetphp/uploads.inials PHP-Konfiguration ein
2026-04-10 – Belegtext-Speicherung für spätere Volltextsuche
Neu
- Migration 021: Spalte
beleg_text MEDIUMTEXTinbelege– speichert den perpdftotextoder Tesseract-OCR extrahierten Volltext KontierungsvorschlagService::extractText(): neue public Methode zur Textextraktion (PDF + Bild), unabhängig vom KI-Vorschlag nutzbar- Beim Generieren des KI-Vorschlags wird
beleg_textautomatisch befüllt (einmalig, nur wenn noch leer) Beleg::saveBelegText(): neue Methode im Model
2026-04-10 – OCR-Schalter unabhängig vom KI-Schalter
Neu
- Einstellungen: separater Schalter „OCR-Texterkennung" (
ocr_aktiv) zum unabhängigen Ein-/Ausschalten der Tesseract-OCR bei Bildbelegen (Migration 020) - KI-Schalter und OCR-Schalter arbeiten nun vollständig unabhängig voneinander
KontierungsvorschlagService::isOcrEnabled(): neue Methode, prüft OCR-Einstellung- Bildbeleg-OCR wird nur noch ausgeführt, wenn
ocr_aktiv = 1; PDF-Textextraktion viapdftotextläuft unabhängig davon
2026-04-02 – Kontenrahmen: Dokumentation und finale Korrekturen
Neu
Kontenrahmen.md: Vollständige Dokumentation aller SKR03-Systemkonten mit UStVA-Kennzahlen und EÜR-Zeilennummern- Verweis auf
Kontenrahmen.mdinREADME.mdergänzt
Geändert (Navigation)
- Kontenrahmen (
/accounts) als eingerückter Untermenüpunkt unter „Einstellungen" in der Seitenleiste - Konten-Tabelle aus
/settingsentfernt (war redundant zu/accounts)
Geändert (UStVA-Kennzahlen, Migration 017)
- KZ
61→66für alle VSt19-Konten (normale Inlands-Vorsteuer, § 15 Abs. 1 Nr. 1 UStG) - KZ
62→66für alle VSt7-Konten - KZ
35→48für 8100/8105/8110 (steuerfreie Umsätze ohne VoSt-Abzug) - KZ
35→43für 8120 (Ausfuhrlieferungen § 4 Nr. 1a UStG)
Geändert (EÜR-Zeilennummern, Migration 019)
- 8400/8570/8720: Z. 14 → Z. 16 (19 %-Erlöse netto; Z. 14 = Kleinunternehmer brutto)
- 8120/8125: Z. 16 → Z. 18 (Ausfuhr/ig. Lieferungen)
- 8950: Z. 18 → Z. 21 (nicht steuerbare Innenumsätze)
- 4672: Z. 43 → Z. 51 (nicht abziehbarer Anteil Unternehmerreisekosten, § 4 Abs. 5 EStG)
- 4288: Z. 52 → Z. 65 (häusl. Arbeitszimmer abziehbar, eigene Zeile seit EÜR 2023)
- 4287: Z. 52 → Z. 66 (Tagespauschale häusl. Wohnung, eigene Zeile seit EÜR 2023)
- 4856: Z. 49 → Z. 37 (Sonderabschreibungen § 7b EStG)
Kanonischer Seed (Migration 018)
018_seed_kontenrahmen_skr03_final.sql: Alle 146 Systemkonten mit bereits korrekten UStVA-KZ und EÜR-Zeilen; Neuinstallationen erhalten den vollständigen aktuellen Stand ohne weitere Korrekturen
2026-04-01 – Feature: EÜR-Zeile in Konten-Übersicht anzeigen
Neu
- Spalte „EÜR-Zeile" in der Konten-Liste (/accounts) ergänzt
- Zeigt den Wert
euer_zeileje Konto an (z.B. 14, 26, 51, 52 oder „–" wenn leer)
2026-04-01 – Feature: Bewirtungsbelege (Konten 4650/4651) mit automatischem Split
Neu
- Buchungen auf Konto 4650 (Bewirtungskosten VSt. 19%) oder 4651 (Bewirtungskosten VSt. 7%) werden automatisch aufgeteilt: 70 % auf das gewählte Konto, 30 % auf das Abzugskonto (4654 bzw. 4656)
- Neue Felder
bewirtungsgrundundteilnehmerlistein derbookings-Tabelle (Migration 016) - Im Buchungsformular erscheinen die beiden Felder automatisch, wenn 4650/4651 als Kategorie gewählt wird
- Vorhandene Buchungen zeigen Bewirtungsgrund und Teilnehmerliste in der Buchungsübersicht an
2026-04-01 – Bugfix: Nicht abzugsfähige Kosten (4654/4656) separat in EÜR unter KZ 165
Behoben
- Konten mit
euer_zeile = '51'(4654, 4656, 4635, 4636, 4289) fließen nicht mehr intotal_ausgabenundgewinnein - Sie erscheinen in einem eigenen Abschnitt „Nicht abzugsfähige Betriebsausgaben (KZ 165)" in Web und PDF
- Migration 015 setzt
euer_zeile = '51'für Konto 4656 (fehlte bisher)
2026-03-30 – Bugfix: 30 % Eigenanteil fließt jetzt korrekt in UStVA KZ 81/83
Behoben
Booking::upsertEigenanteil30: statt zwei Buchungen (8922 Netto + 1776 USt-Korrektur) wird nur noch eine einzige Buchung auf 8922 mit echtemust_satzundbetrag_usterzeugtupsertKorrekturbuchung: neue ParameterustSatzundbetragUst; UPDATE und INSERT setzen jetztbetrag_ustundbetrag_bruttokorrekt- Vorher:
betrag_ust = 0auf 8922 → KZ 83 blieb leer; 1776-Buchung hatte keine UStVA-Wirkung
2026-03-30 – Kontenrahmen-Reset: DATEV SKR03 2026 (IST-Versteuerung)
Geändert
- Alle Buchungen und Belege gelöscht (Testdaten)
- Gesamter Kontenrahmen durch DATEV SKR03 2026 (gültig ab 2026-01-01) ersetzt
- Neue Spalte
euer_zeilein Tabelleaccounts: Zeilennummer der Anlage EÜR - 145 Konten neu angelegt: Klassen 0, 1, 3, 4, 8 (ohne Klasse 9 / ohne Verbindlichkeiten)
- Verfahren: IST-Versteuerung; Konto 1390 „Verrechnungskonto Ist-Versteuerung" enthalten
- UStVA-Kennzeichen korrekt nach DATEV: 81/86 (Umsätze), 61/62 (Vorsteuer), 35/41 (steuerfrei)
- Migration:
013_reset_kontenrahmen_skr03_2026.sql
2026-03-30 – KI-gestützte Belegerkennung (experimentell) + 30 % Eigenanteil Telefon/Internet
Neu
- Eigenanteil von 30 % bei Buchungen auf Telefon- und Internetkosten-Konten (4920/4925) wählbar
- Experimentelle KI-Beleganalyse: erkennt Buchungstyp, Betrag, Konto und Lieferant aus PDF/Bild-Belegen
- Setzt lokales LLM (z. B. Ollama/Mistral) oder Remote-API-Schlüssel voraus
- Hoher Ressourcenbedarf bei lokaler Ausführung; Ergebnisqualität mit Mistral bisher mäßig
- Getestet mit
ollama run mistral
Betrifft
src/Services/KontierungsvorschlagService.phpsrc/Services/LlmService.phpsrc/Views/belege/detail.php,list.phpDockerfile,docker-compose.yml
2026-03-30 – Fix: Lieferant-Eingabe in Metadaten-Leiste speichert korrekt
Geändert
supplier-persistent-input: Nach erfolgreichem AJAX-Speichern wird das Texteingabefeld durch den Inline-Trigger-Span ersetzt – visuelle Bestätigung, dass der Lieferant gespeichert wurde- Fehlerbehandlung (
try/catch) hinzugefügt: bei fehlgeschlagenem Speichern bleibt das Feld editierbar - Das versteckte Metaformular (
supplier_name_input) wird nach AJAX-Speichern synchronisiert, damit ein späteres Absenden des Metaformulars den Lieferanten nicht überschreibt
Betrifft
src/Views/belege/detail.php
2026-03-30 – Lieferant inline editierbar + KI-Lieferantenerkennung
Geändert
- Lieferant in der Beleg-Info-Bar ist jetzt direkt inline editierbar (Klick auf ✎), wie Belegdatum und Rechnungsnr.
- KI-Kontierungsvorschlag extrahiert nun auch den Lieferantennamen aus dem Beleg
- Nach KI-Analyse: bekannter Lieferant wird automatisch eingetragen; unbekannter Lieferant öffnet das Lieferantenfeld sofort mit Fokus
- KI-Badge zeigt Lieferantenname an, unbekannte Lieferanten werden als „(neu – bitte prüfen)" markiert
- Validierung in
KontierungsvorschlagServiceprüftzahlungskonto_idundkategorie_konto_idjetzt gegen die korrekte Kontenart statt gegen alle Konten
Betrifft
src/Services/KontierungsvorschlagService.phpsrc/Views/belege/detail.phppublic/static/js/app.js
2026-03-29 – EÜR: Spaltenauswahl nach Kontenart statt Buchungstyp
Geändert
EuerCalculator::calculate()wertetaccounts.kontenartaus stattbuchungstypBetriebseinnahme→ Einnahmen-Spalte,Betriebsausgabe→ Ausgaben-SpaltePrivat,Zahlungskonto,Verbindlichkeitals Kategorie-Konto werden nicht in der EÜR ausgewiesen- Vorzeichen von
betrag_nettowerden beibehalten (keinabs()mehr) – negative Beträge mindern die jeweilige Spalte - Buchungen desselben Kontos werden unabhängig vom
buchungstypzu einem Eintrag zusammengefasst
Betrifft
src/Services/EuerCalculator.phpBERICHTE.md(EÜR-Kapitel aktualisiert)
2026-03-29 – UStVA: kontonummernbasierte Kennzahlenberechnung
Geändert
UstvaCalculator::calculate()wertet ausschließlichaccounts.ustva_kzdes Kategorie-Kontos aus –buchungstypundsteuerartwerden nicht mehr benötigtUstvaCalculator::fetchBookingsForKz()filtert überk.ustva_kz IN (...)statt über Buchungsfelder- Kz 83 und 84 kommen jetzt direkt aus
betrag_ustder Erlöskonten (stattKz81 × 19 %) - Kz 91 und 94 kommen direkt aus
betrag_ustder ig.-Erwerb-Konten (statt abgeleitet)
Betrifft
src/Services/UstvaCalculator.php- Migration
010_ustva_kz_konto_mapping.sql BERICHTE.md(UStVA-Kapitel vollständig neu)
Migration 010 – Kontenanpassungen
- Alle Betriebsausgabe-Konten mit
steuercode='VSt19':ustva_kzvon'66'auf'61' - Alle Betriebsausgabe-Konten mit
steuercode='VSt7':ustva_kzvon'66'auf'62' - Neue Konten: 3425 „Innergemeinschaftliche Erwerbe 19 %" (
ustva_kz='89'), 3420 „Innergemeinschaftliche Erwerbe 7 %" (ustva_kz='93')
2026-03-28 – 30% Eigenanteil-Kennzeichnung und Korrekturbuchungen
Hinzugefügt
- Checkbox „30% Eigenanteil buchen" in der Buchungszeilen-Erfassung (zwischen Nettobetrag und Submit-Button)
- Neues Feld
eigenanteil_30 TINYINT(1)in derbelege-Tabelle (Migration009_beleg_eigenanteil_30.sql) - Der Checkbox-Wert wird beim Speichern einer Buchung in die Beleg-Metadaten übernommen
- Bei gesetzter Checkbox werden automatisch zwei Korrekturbuchungen angelegt oder aktualisiert:
- 30% des Nettobetrags an Konto 8922 (nicht abzugsfähiger Eigenanteil), Zahlungskonto 1800
- 30% der errechneten USt an Konto 1776 (Vorsteuerkorrektur), Zahlungskonto 1800 – nur wenn USt-Satz > 0
- Im Testmodus: bestehende Korrekturbuchungen werden direkt überschrieben
- Im Produktivmodus: bestehende Korrekturbuchungen werden GoBD-konform storniert und neu angelegt
Betrifft
src/Views/belege/detail.phpsrc/Controllers/BookingController.phpsrc/Models/Booking.php(neue MethodenupsertEigenanteil30,upsertKorrekturbuchung)- Migration:
009_beleg_eigenanteil_30.sql
2026-03-27 – DATEV-konformer SKR03 Kontenrahmen (#11)
Geändert
- Kontenrahmen: Bisherige ~80 Konten durch DATEV-konformen SKR03 ersetzt (Migration
008_skr03_vollstaendig.sql) - Umfang: 157 Systemkonten aus Klassen 0 (Anlagevermögen), 1 (Zahlungsmittel, Vorsteuer, USt, Privat), 2 (Eigenkapital), 4 (Betriebsausgaben), 8 (Betriebseinnahmen)
- EÜR-Fokus: Keine Forderungs- oder Verbindlichkeitskonten (nicht bilanzierend)
- Kompatibilität: Alte Konten bleiben mit
is_system = 0erhalten – bestehende Buchungen sind nicht betroffen
Betrifft
- Migration:
008_skr03_vollstaendig.sql
2026-03-27 – Ausgangsrechnungen in Dashboard/EÜR/UStVA (#8)
Behoben
- Seed-Skript:
scripts/seed-testdata.phpriefInvoice::finalize()auf, ohne anschließend Beleg und Buchungen anzulegen – Ausgangsrechnungen fehlten daher in allen Auswertungen - Ursache:
Invoice::finalize()(Model) setzt nur Status und Rechnungsnummer; die Buchungserstellung liegt inInvoiceController::createBelegAndBookings()und wurde im Seed nicht aufgerufen - Fix: Seed-Skript legt nach
Invoice::finalize()nun Beleg und zugehörige Buchungen an (analog zur Controller-Logik), sodass Ausgangsrechnungen korrekt in EÜR, UStVA und Dashboard erscheinen
Betrifft
- Skript:
scripts/seed-testdata.php - Test:
tests/09_InvoiceToReportsTest.php(neu – End-to-End-Test Ausgangsrechnung → EÜR/UStVA/Dashboard)
2026-03-26 – Lieferantenmanagement (#7)
Neu
- Lieferantenverwaltung: Neue Seite
/supplierszum Anlegen, Bearbeiten und Löschen von Lieferanten - Lieferant am Beleg: Im Metadaten-Formular der Belegdetailseite kann ein Lieferant per Dropdown/Freitexteingabe ausgewählt werden; unbekannte Namen werden beim Speichern automatisch angelegt
- Datenbank: Tabelle
suppliers(id, name) und Fremdschlüsselsupplier_idinbelege - Navigation: Neuer Menüpunkt "Lieferanten" im Bereich Belege & Buchungen
Betrifft
- Migration:
007_suppliers.sql - Model:
Supplier.php(neu),Beleg.php(JOIN + updateMeta) - Controller:
SupplierController.php(neu),BelegController.php - Views:
suppliers/list.php,suppliers/form.php(neu),belege/detail.php,layout/nav.php - Routes:
public/index.php - Test:
08_LieferantTest.php
2026-03-26 – EÜR-Buchungsmodell (Soll/Haben → Einnahme/Ausgabe)
Geändert
- Buchungsmodell: Doppelte Buchführung (Soll-Konto/Haben-Konto) durch reines EÜR-Modell ersetzt – jede Buchung hat nun
buchungstyp(Einnahme/Ausgabe),kategorie_konto_id(Was?) undzahlungskonto_id(Wie bezahlt?) - Kontenarten: Umbenannt von Aktiv/Passiv/Aufwand/Ertrag/Neutral zu Betriebseinnahme/Betriebsausgabe/Zahlungskonto/Verbindlichkeit/Privat
- EÜR-Berechnung: Vereinfacht – gruppiert direkt nach
buchungstypstatt komplexer Soll/Haben-Ableitung - UStVA-Berechnung: Verwendet
buchungstypstatt Kontenart-Ableitung für USt/VSt-Zuordnung - UI-Terminologie: Alle Bezeichnungen in Views, Journal, Dashboard und Formularen von Soll/Haben auf Typ/Kategorie/Zahlungskonto umgestellt
- CSV-Export: Spalten an neues Modell angepasst (Typ, Kategorie-Kto., Zahlungskonto-Kto.)
Betrifft
- Schema (
001_initial_schema.sql), Seed (002_seed_skr03.sql) - Models:
Booking.php,Account.php - Services:
EuerCalculator.php,UstvaCalculator.php - Controllers:
BookingController.php,AccountController.php,InvoiceController.php - Views:
dashboard.php,journal.php,detail.php,accounts/form.php,accounts/list.php - JavaScript:
app.js - Tests: 02, 04, 05 aktualisiert + neuer Test 06 (EÜR-Buchungsmodell)
2026-03-25 – Session-Authentifizierung & Benutzerverwaltung
Features
- Session-Authentifizierung: HTTP Basic Auth durch cookie-basierte Session-Authentifizierung ersetzt; Sessions laufen nach 24 Stunden absolut ab
- Benutzerverwaltung (
/useradmin): Benutzer anlegen, bearbeiten (Benutzername und Passwort), deaktivieren und löschen; Passwörter mit bcrypt gehasht - Ersteinrichtungsflow: Beim ersten Start ohne Benutzer ermöglicht
/logindas Anlegen des ersten Administrator-Accounts - Abmelden-Button in der Seitenleiste
Geändert
AUTH_USERundAUTH_PASSentfernt – Zugangsdaten werden ausschließlich in der Datenbank verwaltet- Neue Datenbanktabelle
users(Migration005_users.sql)
Behoben
- Fehler in
BelegControllerbehoben
2026-03-25 – Sicherheit, UX-Verbesserungen & Bugfixes
Sicherheit
- Timing-sicherer Login: Passwort- und Benutzernamevergleich auf
hash_equals()umgestellt (verhindert Timing-Angriffe) - SVG-Upload gesperrt: Logo-Upload akzeptiert nur noch PNG und JPEG – SVG wurde entfernt (XSS-Risiko)
- CSV-Injection-Schutz: Felder im Buchungsjournal-CSV-Export werden gegen Formula-Injection (
=,+,-,@) abgesichert
Features
- Inline-Editing: Belegdatum und Rechnungsnr. können direkt in der Beleg-Detailansicht per Klick bearbeitet werden (kein Seitenreload)
- Belegart-Filter: Neue Filteroption in der Belegliste zum Filtern nach Belegart (Eingangsrechnung, Ausgangsrechnung, Quittung etc.)
- Favicon & App-Icon: Favicon für Browser-Tabs, Apple Touch Icon und Web-App-Manifest eingebunden
Robustheit
- Datenbankfehler-Handling: PDO-Exceptions in Invoice-, Booking- und BelegController werden separat abgefangen und geloggt statt als generische Fehler zu erscheinen
2026-03-24 – Initiales Release
Features
- Basis-System: MVC-Framework, Routing, Authentifizierung, Datenbank-Migrations-System
- Ausgangsrechnungen: Erstellen, Verwalten und PDF-Export von Ausgangsrechnungen
- EÜR-Vorbereitung: Einnahmen-Überschuss-Rechnung als PDF-Export
- UStVA-Vorbereitung: Umsatzsteuer-Voranmeldung inkl. XML-Export für elster.de