115 KiB
115 KiB
Changelog
2026-08-02 – Dashboard rechnet wie die EÜR, Ereignisse zeigen alle Fehler, kompaktere Buchungszeilen
Dashboard-Zahlen stimmen mit der EÜR überein und lassen sich aufschlüsseln (#167)
- Das Dashboard summierte nach
bookings.buchungstyp, die EÜR nachaccounts.kontenart– für 2026 wich das Ergebnis um 1.241,17 € ab (16.903,50 € statt 18.144,67 €). Im Einzelnen zählte das Dashboard 818,79 € Vorsteuer (Konten 1571/1576/1574, Kontenart „Zahlungskonto") und 350,00 € Anlagenzugang als Betriebsausgabe, ließ AfA-Buchungen komplett aus (buchungstyp='AfA'kam in keiner der beiden Abfragen vor) und minderte das Ergebnis um 389,42 € nicht abzugsfähige Bewirtungskosten, die die EÜR getrennt ausweist. src/Services/EuerCalculator.php: Neue Methodensummary($jahr, $monat = null)undfetchBookingsForBucket($jahr, $monat, $gruppe). Beide leiten ihre Bedingung aus denselben Konstanten ab wiecalculate()– Zuordnungsregeln stehen damit an genau einer Stelle.src/Controllers/AccountController.php:dashboard()bezieht Umsatz, Ausgaben und Ergebnis für Jahr, Vormonat und laufenden Monat aussummary(); die sechs handgeschriebenen SQL-Abfragen entfallen. Neue ActionbucketBookings()für die Detailansicht.src/Views/dashboard.php,src/Views/dashboard_bucket.php(neu): Alle neun Zahlen der ersten drei Kacheln verlinken auf/dashboard/buchungen/{gruppe}/{jahr}[/{monat}]. Die Liste zeigt Datum, Buchungsnummer, Beleg, Text und Konten mit Summenzeile; in der Ergebnisansicht stehen Betriebsausgaben mit negativem Vorzeichen, sodass die Spaltensumme dem Kachelwert entspricht. Nicht abzugsfähige Ausgaben werden unterhalb der Liste beziffert statt stillschweigend weggelassen.tests/80_DashboardEuerAbgleichTest.php(neu),tests/e2e/specs/dashboard-detail.spec.js(neu): Der PHP-Test bucht Aufwand, AfA, Vorsteuer und nicht abzugsfähige Bewirtung und prüft, was davon wo landet; der E2E-Test vergleicht den angezeigten Kachelwert mit der Summe der verlinkten Liste.
Ereignisse umfassen alle Fehler und Warnungen (#165)
- Die Ereignisliste kannte nur den Nextcloud-Import. Fehler an anderer Stelle – etwa ein gescheiterter Beleg-Upload – waren nur als kurz sichtbare Meldung im Browser oder im Server-Log nachvollziehbar.
migrations/091_system_events_level.sql(neu): Spaltelevel(info/warning/error) samt Indizes. Bestehende Einträge werden anhand ihres Textpräfixes eingestuft, nach dem die Liste sie bisher eingefärbt hat.src/Models/EventLog.php:add()nimmt einen Schweregrad entgegen, dazuwarning(),error(),find()mit Filter nach Bereich und Schweregrad sowiechannels().tryAdd()schreibt das Ereignis, ohne selbst scheitern zu können – nötig für Aufrufer, die bereits einen Fehler behandeln.- Neue Fehlerquellen:
View::redirect()protokolliert jede Meldung vom Typerroroderwarningim Bereichmeldungen– damit ist jede dem Nutzer angezeigte Fehlermeldung erfasst, ohne sie einzeln anbinden zu müssen.BelegController::renderUploadErrors()schreibt zusätzlich im Bereichbeleg-uploadmit Dateiname,public/index.phpprotokolliert unbehandelte Ausnahmen und schwere PHP-Fehler im Bereichsystem. src/Controllers/EventController.php,src/Views/ereignisse/index.php,src/Views/ereignisse/_body.php: Aus der Nextcloud-Seite wird eine Übersicht über alle Bereiche mit zwei Auswahlfeldern (Bereich, Schweregrad). Der automatische Neuaufbau alle 60 Sekunden behält den eingestellten Filter./ereignisse/nextcloud-importleitet auf die gefilterte Übersicht weiter, der bisherige Daten-Endpunkt bleibt erreichbar.tests/81_EreignisseAlleFehlerTest.php(neu),tests/e2e/specs/ereignisse.spec.js(neu),tests/58_EreignisAutoReloadTest.php(angepasst).
Buchungszeilen brauchen weniger Breite (#166)
src/Views/belege/detail.php: Buchungsnummer und Buchungsdatum teilen sich in der Belegansicht eine Spalte (Nummer oben, Datum darunter) – wie bereits bei Kategorie- und Zahlungskonto. Die Tabelle ist damit eine Spalte schmaler und ohne waagerechtes Scrollen lesbar.tests/e2e/specs/beleg-buchung.spec.js: Neuer Test auf die zusammengelegte Spalte; die Betragsprüfungen der bestehenden Tests greifen jetzt auf die verschobene Zelle zu.
Nebenbei
docs/berechnungslogik.md: Die Tabelle der EÜR-Zuordnung nannte für nicht abzugsfähige Ausgaben nocheuer_zeile='51'; seit Migration 075 ist es'74', und51ist eine gewöhnliche Ausgabenzeile (u.a. Bürobedarf 4930). Korrigiert und um Anlagevermögen, Vorsteuerkonten und die Dashboard-Kacheln ergänzt.
2026-08-02 – Testläufe hinterlassen keine Verzeichnisse mehr, die echte Uploads blockieren
- Die Testsuite lief über
docker compose execals root, Apache/PHP dagegen alswww-data. Legte ein Test unterhalb vondata/ein Verzeichnis an, gehörte es root und war für die Anwendung nicht beschreibbar. Weil der Rollback nur die Datenbank zurückdreht, wurde die im Test vergebene Belegnummer danach erneut vergeben – der nächste echte Upload traf auf das bestehende Verzeichnis, übersprang dasmkdirund scheiterte inmove_uploaded_file()mit HTTP 500. In #168 war nur der konkrete Verursacher entschärft worden, nicht die Ursache. - Aufrufkonvention: Die PHP-Suite läuft jetzt mit
docker compose exec -u www-data web php tests/run.php, also unter derselben Kennung wie die Anwendung. Zurückbleibende Dateien hätten damit die richtigen Rechte und wären unschädlich. Angepasst inCLAUDE.md,README.md,docs/automatisierung.md,tests/e2e/README.md,scripts/update.shsowiescripts/run-tests.sh– letzteres wechselt selbst persetprivaufwww-data, wenn es als root im Container startet. tests/TestCase.php: Neue MethodenregisterTempPath(),makeTempDir()undtempPath(). Angemeldete Pfade entfernttearDown()rekursiv – auch nach einer Ausnahme, auch bei schreibgeschützten Verzeichnissen und inklusive der vonmkdirnebenbei angelegten Elternverzeichnisse. Als Sicherheitsnetz wird ausschließlich unterhalb des Temp-, Beleg- und Logoverzeichnisses gelöscht und nie eines dieser Verzeichnisse selbst; alles andere bleibt liegen und wird gemeldet.tests/run.php: Vergleicht den Bestand der Datenverzeichnisse vor und nach dem Lauf und meldet am Ende jeden Rückstand, den ein Test nicht angemeldet hat. Damit fällt ein künftiger Test auf, statt still Leichen zu hinterlassen.tests/19,33,34,35,49,66,67,70,76: Alle Tests mit Dateisystem-Zugriff auf die zentrale Registrierung umgestellt; die verstreutenunlink()/rmdir()-Aufrufe und der eigenetearDown()in Test 19 entfallen. Test 66 hinterließ zusätzlich die vontempnam()angelegte Datei, weil das Suffix erst danach angehängt wurde.src/Controllers/BelegController.php:upload()prüft vor dem Speichern, ob das Zielverzeichnis existiert und beschreibbar ist, und wertet den Rückgabewert vonmove_uploaded_file()aus. Statt einer Ausnahme mit HTTP 500 erscheint jetzt eine verständliche Meldung im Formular (bzw. als JSON bei AJAX-Uploads), und die bereits vergebene Belegnummer wird überBeleg::releaseBelegNr()wieder freigegeben.tests/79_TestAufraeumenTest.php(neu): Sichert das Aufräumen samt Sicherheitsnetz und die Härtung des Uploads ab.
2026-08-02 – Nummernkreise geben die zuletzt vergebene Nummer beim Löschen wieder frei
- Der E2E-Finalisierungstest verbrauchte bei jedem Lauf eine Rechnungs- und eine Belegnummer: Die Testrechnung wurde zwar wieder gelöscht, die Nummern blieben aber vergeben und rissen dauerhaft Lücken in den laufenden Nummernkreis.
src/Models/Invoice.php,src/Models/Beleg.php:delete()gibt die Nummer des gelöschten Datensatzes jetzt wieder frei. Die zuletzt vergebene Nummer wird dafür aus dem Zählerstand rekonstruiert und nur bei exakter Übereinstimmung zurückgezählt – existiert bereits eine jüngere Rechnung bzw. ein jüngerer Beleg (oder stammt die Nummer aus einem früheren Monat/Jahr), bleibt der Zähler stehen. Damit kann keine Nummer doppelt vergeben werden.Invoice::delete()gibt zusätzlich die Belegnummer des mitgelöschten Rechnungsbelegs frei.scripts/e2e-cleanup.php: Räumt liegengebliebene Testdaten absteigend ab (jüngste zuerst), damit die Freigabe für die ganze Kette greift und nicht nur für den letzten Datensatz.tests/78_NummernkreisFreigabeTest.php(neu): Sichert Freigabe und Wiedervergabe für beide Nummernkreise ab – inklusive der Gegenprobe, dass das Löschen eines älteren Datensatzes den Zähler unverändert lässt.tests/e2e/specs/rechnung-export.spec.js: Neuer TestLöschen einer finalisierten Rechnung gibt die Nummer wieder freiweist über zwei aufeinanderfolgende Finalisierungen nach, dass der Testlauf den Nummernkreis nicht mehr weiterschiebt. Der Hinweis auf die „nicht umkehrbare Nebenwirkung" intests/e2e/README.mdentfällt damit.
2026-08-02 – E2E-Tests: Browser-Ebene abgedeckt, zwei kaputte Seiten repariert
- Die PHP-Suite prüft Models und Services, ruft aber nie einen Controller über HTTP auf. Routing, Views und das JavaScript in den Formularen waren damit vollständig ungetestet – genau die Ebene, auf der die Fehler der letzten Wochen auftraten. Steuerkram ist jetzt an den generischen e2e-runner angebunden; auf dem Host wird dafür nichts installiert.
src/Controllers/AccountController.php:/accounts/newlieferte HTTP 500.create()las$_GET['from']ohne Existenzprüfung – beim Aufruf aus dem Menü (ohne Parameter) also immer. Jahrelang war das eine stille PHP-Warning; seit der Fehler-Härtung (#142) wandelt derset_error_handlerWarnings in Exceptions um, seitdem war die Seite tot. Jetzt mit?? ''abgesichert.src/Controllers/AssetController.php,src/Views/assets/form.php:/assets/newlieferte aus demselben Grund HTTP 500. Die View nutzt$testModeund$defaultKontoIdunterhalb des Bearbeiten-Zweigs, gesetzt wurden beide aber nur innerhalb davon.create()übergibt jetzttestMode, die View initialisiert beide Werte am Kopf.tests/e2e/(neu): 44 Playwright-Tests.smoke.spec.jsprüft, dass alle Hauptseiten ohne Serverfehler rendern (dieser Lauf fand die beiden 500er), dazu 404-Verhalten, Login-Zwang und CSV-Export.beleg-buchung.spec.jsdeckt Upload → Buchungsmaske → gespeicherte Buchung ab, insbesondere den Netto/Brutto-Umschalter (#137), dessen Umrechnung erst im submit-Handler passiert und serverseitig nicht prüfbar ist.rechnung-export.spec.jsprüft Entwurf → Finalisieren → PDF- und XML-Export.bank-import.spec.jssichert die Vorschau samt XSS-Regression (#133) ab, die vollständig im Browser aufgebaut wird.scripts/e2e-user.php,scripts/e2e-cleanup.php(neu): Legen den Testnutzer idempotent an bzw. entfernen liegengebliebene, mitE2E-TESTmarkierte Testdaten. Die Tests laufen gegen eine Instanz mit echten Arbeitsdaten und rühren ausschließlich ihre eigenen Datensätze an.tests/77_BelegLoeschsperreTest.php(neu): Sichert ab, dassBeleg::delete()einen Beleg mit Buchungen und einen festgeschriebenen Beleg verweigert und dabei die Buchungen unangetastet lässt. Bewusst als PHP-Test – reine Model-Logik gehört nicht in den Browser.tests/67_BelegXmlXsltXssTest.php: Der Test legte ein Belegverzeichnis an und löschte am Ende nur die Datei, nicht das Verzeichnis. Die Transaktion rollt die Belegnummer zurück, das Dateisystem aber nicht – und weil die Suite überdocker compose execals root läuft, blieb ein root-eigenes Verzeichnis zurück. Bekam später ein echter Upload dieselbe Belegnummer, scheitertemove_uploaded_file()mit „Permission denied" und die Seite antwortete mit HTTP 500. Das traf nicht nur Tests, sondern den normalen Beleg-Upload nach jedem lokalen Testlauf. Der Test räumt das Verzeichnis jetzt mit ab.- Fehlt Docker, das Runner-Projekt oder das Playwright-Image, überspringt
tests/e2e/run.shmit Exit 0 und einer Warnung. Die E2E-Tests bleiben damit optional und blockieren keine Umgebung ohne Runner.
2026-08-01 – Belege blieben nach dem Umzug nach data/ unauffindbar
- Beim Umzug der Nutzdaten nach
data/(#150) wurden die Belege mitmv uploads/2026 data/belege/verschoben. Existierte im Ziel bereits ein Jahresverzeichnis – etwa weil vorher schon ein Container gelaufen war –, scheitertemv(bzw. legte den Baum alsdata/belege/2026/2026ab). Weil alle Fehler mit2>/dev/null || trueunterdrückt wurden, meldete die Migration trotzdem Erfolg: In der Oberfläche waren die Belege danach nicht mehr auffindbar, obwohl die Datenbankeinträge unverändert waren. scripts/uploads-uebernahme.sh(neu): Übernimmt den Altbestand jetzt Datei für Datei und führt bestehende Verzeichnisse zusammen, statt ganze Verzeichnisse zu verschieben. Eine bereits vorhandene Zieldatei wird nie überschrieben – die Quelldatei bleibt inuploads/zur Prüfung liegen und die Kollision wird gemeldet. Verbleiben Reste, endet das Skript mit Exit-Code 1 statt mit einer Erfolgsmeldung.docker-entrypoint.sh,scripts/migrate-datenverzeichnis.sh: Beide rufen dieselbe Übernahme-Logik auf, damit Container-Start und manueller Umzug nicht auseinanderdriften. Dascripts/als Volume gemountet ist, der Entrypoint dagegen aus dem Image stammt, wirkt eine Korrektur am Umzug ohne Rebuild.tests/76_DatenverzeichnisUebernahmeTest.php(neu): Führt die Übernahme real gegen ein Zielverzeichnis aus, in dem das Jahresverzeichnis bereits existiert, und sichert Zusammenführung statt Verschachtelung, unangetastete Zieldateien, die Trennung von Logo und Belegen sowie die Exit-Codes ab.
2026-08-01 – Analyse-Skript für die Nextcloud-Verbindung
scripts/analyse_nextcloud.sh(neu): Grenzt Störungen des Belegimports ein, ohne dass dafür Einzeiler auf dem Server nötig sind. Das Skript liest die hinterlegte Konfiguration (Passwort nur als Länge und Verschlüsselungsstatus, nie im Klartext), zeigt Namensauflösung und IP-Pinning des SSRF-Schutzes, unternimmt einen PROPFIND auf den Import-Ordner, listet dessen Inhalt inklusive der Dateien, die wegen ihres Typs übersprungen würden, und prüft die Erreichbarkeit des Erledigte-Ordners.- Schlägt der Verbindungsversuch fehl, folgen automatisch Gegenproben – ohne IP-Pinning sowie je Adressfamilie einzeln – und ein Befund, der zwischen abgewiesener Anmeldung (401/403), fehlendem Ordner (404), einer nicht nutzbaren Adressfamilie und einem generell nicht erreichbaren Server unterscheidet. Die cURL-Optionen entsprechen bewusst denen von
NextcloudImportService::newCurl(), damit die Analyse den echten Importlauf abbildet. - Rein lesend: keine schreibenden WebDAV-Methoden, kein Importlauf – bei der Fehlersuche werden also keine Belege verschoben oder gelöscht. Exit-Code 0 bedeutet, dass die Verbindung steht. Läuft das Skript im Container, ruft es
phpdirekt auf, sonst überdocker compose exec. tests/75_AnalyseNextcloudSkriptTest.php(neu): Sichert bash-Syntax, Ausführbarkeit und vor allem zu, dass das Skript lesend bleibt und das Passwort nicht ausgibt.docs/konfiguration.md: Abschnitt „Verbindung analysieren" unter „Nextcloud-Import einrichten".
2026-08-01 – Nextcloud-Belegimport wieder funktionsfähig (IP-Pinning bei Dual-Stack-Hosts)
- Seit dem Deployment des SSRF-Schutzes (#130) schlug der Nextcloud-Import bei jedem Lauf mit „PROPFIND fehlgeschlagen. URL/Zugangsdaten prüfen." fehl. Ursache war nicht die Konfiguration, sondern das IP-Pinning:
SsrfGuard::resolvePinned()gab pro aufgelöster IP-Adresse einen eigenenCURLOPT_RESOLVE-Eintrag zurück. libcurl behandelt mehrere Einträge für dieselbeHOST:PORT-Kombination aber nicht als Alternativen – der zuletzt gesetzte verdrängt die vorherigen. Bei einem Host mit A- und AAAA-Record blieb dadurch nur die IPv6-Adresse übrig, und da der Container keine IPv6-Konnektivität hat, scheiterte jede Verbindung sofort mit „Could not connect to server". src/Services/SsrfGuard.php:resolvePinned()liefert jetzt genau einen Eintrag mit allen geprüften Adressen als Liste (HOST:PORT:IP1,IP2,…), IPv6 in eckigen Klammern. cURL probiert die Adressen damit wie bei eigener Auflösung nacheinander durch und fällt auf IPv4 zurück. Der DNS-Rebinding-Schutz bleibt unverändert – gepinnt wird weiterhin ausschließlich auf die zuvor validierten Adressen.src/Services/NextcloudImportService.php: Fehlgeschlagene Requests landeten bisher ohne jedes Detail im Ereignis-Log, was die Eingrenzung ohne Server-Zugriff unmöglich machte. PROPFIND- und Download-Fehler nennen jetzt den cURL-Fehlercode bzw. den HTTP-Status samt tatsächlich kontaktierter Gegenstelle (neue MethodedescribeTransfer()).tests/74_SsrfPinningDualStackTest.php(neu): Stellt sicher, dassresolvePinned()genau einen Eintrag liefert, beide Adressfamilien eines Dual-Stack-Hosts darin enthalten sind und IPv6-Adressen geklammert werden.
2026-07-31 – Rechnungsstatus folgt automatisch den erfassten Zahlungen
- Bisher blieb eine Ausgangsrechnung auch dann auf „Gesendet", wenn dem zugehörigen Beleg bereits Zahlungsbuchungen zugeordnet waren – der Status wurde ausschließlich manuell über „Rechnung als bezahlt markieren" gesetzt. Neu leitet sich der Status aus der Summe der zugeordneten Buchungen ab: Teilzahlung (weniger als der Rechnungsbetrag), Bezahlt (exakt der Rechnungsbetrag) und Überzahlt (mehr als der Rechnungsbetrag). Werden alle Buchungen storniert, fällt die Rechnung auf „Gesendet" bzw. „Finalisiert" zurück.
migrations/090_invoice_zahlstatus.sql(neu): Erweitert das Status-ENUM umTeilzahlungundÜberzahlt, ergänzt die Spaltesent_at(damit beim Zurücksetzen zwischen „Finalisiert" und „Gesendet" unterschieden werden kann) und rechnet den Status vorhandener Rechnungen einmalig aus deren Buchungen nach. Manuell auf „Bezahlt" gesetzte sowie stornierte Rechnungen bleiben unangetastet.src/Models/Invoice.php: Neue MethodenrecalcPaymentStatus()(Statusableitung inkl. Vorzeichennormalisierung für Stornorechnungen mit negativen Beträgen) undrecalcPaymentStatusForBeleg(). Entwürfe und stornierte Rechnungen werden nie überschrieben.src/Models/Booking.php:create()undreverse()stoßen die Neuberechnung nach dem Commit an, ebenso das Löschen einer Buchung im Testmodus (BookingController::destroy()). Damit greift die Statusberechnung unabhängig davon, ob über die Rechnung, den Beleg oder die Bankzuordnung gebucht wird.sumBruttoForBeleg()kann jetzt auf einen Buchungstyp eingeschränkt werden;sumZahlungen()summiert die aktiven Einnahme-Hauptbuchungen einer bereits geladenen Liste.src/Views/invoices/show.php,book.php: Das Zahlungs-Modal und die Buchungsansicht zeigen unter den Buchungen jetzt Rechnungsbetrag, gezahlten Betrag und den offenen Rest (bzw. den überzahlten Betrag). Die Betragsvorbelegung im Modal entspricht dem offenen Restbetrag statt immer dem vollen Rechnungsnetto, der USt-Satz wird aus der Rechnung vorbelegt.- Statusanzeige in Rechnungsliste, Detailansicht, Buchungsansicht und Kundenakte um die neuen Status ergänzt (
badge-orangeneu für Teilzahlung,badge-purplefür Überzahlt), Statusfilter der Rechnungsliste umfasst jetzt alle sieben Status. src/Controllers/AccountController.php: Die Kennzahl „offene Rechnungen" auf dem Dashboard berücksichtigt Teilzahlungen und weist nur noch den tatsächlich offenen Restbetrag aus.tests/73_InvoiceZahlungsstatusTest.php(neu): Deckt Teilzahlung, Vollzahlung, Überzahlung, Stornos und den Rückfall auf „Gesendet"/„Finalisiert" ab.
2026-07-31 – XML-Rechnung (EN 16931) als Download in der Rechnungs-Detailansicht
- Neben dem PDF-Symbol steht in der Detailansicht einer Rechnung jetzt ein XML-Symbol, das die Rechnungsdaten als reine EN16931-XML-Datei (
Rechnung-<Nr>.xmlbzw.Stornorechnung-<Nr>.xml) zum Download anbietet. Bisher war das EN16931-XML nur in die PDF eingebettet und nicht einzeln zu bekommen. src/Services/ZugferdService.php: Der Dokumentaufbau steckt jetzt inbuildDocument();attachToPdf()bettet das Ergebnis wie bisher in die PDF ein, neu liefertcreateXml()dasselbe Dokument als XML-String. Alle Feldzugriffe sind gegen fehlende Schlüssel undnullabgesichert, sodass auch unvollständige Rechnungen (z.B. Entwürfe ohne Rechnungsnummer) ein wohlgeformtes XML ergeben statt eines Fehlers.- Neue Methode
collectMissingData(): Prüft die für EN 16931 relevanten Angaben und liefert die fehlenden getrennt nach Stammdaten (BT-27, BT-35/37/38, BT-31/32, BT-84) und Rechnungsdaten (BT-1, BT-2, BT-9, BT-10, BT-44, BT-50/52/53/55, BT-72, BG-25) zurück. src/Controllers/InvoiceController.php,public/index.php: Neue Actionxml()und RouteGET /invoices/{id}/xml; die Detailansicht bekommt zusätzlich die Liste der fehlenden Angaben übergeben.src/Views/invoices/show.php: XML-Download-Symbol neben dem PDF-Symbol (auch bei Entwürfen sichtbar). Fehlen Angaben, erscheint über der Aktionsleiste ein Hinweisbanner mit den betroffenen Feldern samt BT-Nummer, und das Symbol wird in Warnfarbe dargestellt – der Download bleibt in jedem Fall möglich.- Fix nebenbei:
mapEinheit()verglich Einheiten ohne den im UI üblichen Punkt („Std.", „Stk."), sodass Stunden im ZUGFeRD-XML bislang als Stück (C62) statt alsHURausgewiesen wurden. Das betraf auch das in die PDF eingebettete XML. tests/72_InvoiceXmlExportTest.php(neu): Prüft Profilkennung, Rechnungsnummer, Käufer- und Verkäuferdaten, Positionen, Bruttosumme und Einheiten-Mapping im erzeugten XML sowie die Meldung fehlender Stamm-/Rechnungsdaten und die Anbindung in Route und Detailansicht.
Nachtrag: Konformität zur XRechnung-CIUS hergestellt
Der E-Rechnungsvalidator (KoSIT-Prüfkonfiguration) meldete für die heruntergeladene Datei einen Fehler und eine Warnung. Die EN16931-Schematron-Prüfung war bereits fehlerfrei – beanstandet wurden ausschließlich die zusätzlichen deutschen Regeln der CIUS XRechnung:
- BR-DE-2 (Fehler): Die Gruppe „SELLER CONTACT" (BG-6) fehlte.
ZugferdServicesetzt jetzt Ansprechpartner (BT-41, ausinhabermit Rückfall auffirma), Telefon (BT-42) und E-Mail (BT-43) aus den Firmeneinstellungen. - BR-DE-21 (Warnung): Die Spezifikationskennung (BT-24) soll der XRechnung entsprechen. Der XML-Download verwendet dafür jetzt das Profil XRechnung 3.0 (
ZugferdProfiles::PROFILE_XRECHNUNG_3) statt des reinen EN16931-Profils. XRechnung ist eine CIUS von EN 16931, die Datei bleibt damit normkonform und erfüllt zusätzlich die BR-DE-Regeln. - PEPPOL-EN16931-R001/R010/R020 (Hinweise): Geschäftsprozess (BT-23) sowie die elektronischen Adressen von Verkäufer (BT-34) und Käufer (BT-49, aus der Kunden-E-Mail) werden jetzt mit ausgegeben.
- Die PDF-Einbettung bleibt unverändert beim Profil EN16931 (Factur-X,
factur-x.xml) – dem üblichen Format für hybride B2B-Rechnungen. Die neuen Kontakt- und Adressangaben stehen aber auch dort, das PDF-XML wird dadurch nur vollständiger. collectMissingData()prüft zusätzlich Ansprechpartner (BT-41), Telefon (BT-42), E-Mail (BT-43/BT-34) und die Kunden-E-Mail (BT-49); Test 72 deckt die neuen Elemente sowie eine XSD-Prüfung des erzeugten XML ab.- Fix: USt-IdNr. des Kunden (BT-48) fehlte immer im XML.
Invoice::find()selektierte vom Kunden zwar Adresse, E-Mail und Zahlungsziel, aber nichtc.ust_id– undinvoiceshat keine eigene Spalte dafür. Der ZweigaddDocumentBuyerVATRegistrationNumber()inZugferdServicelief deshalb nie, auch wenn beim Kunden eine USt-IdNr. gepflegt war (Bestandsfehler, betraf auch das in die PDF eingebettete XML). Bei inländischen Leistungen mit Steuerkategorie „S" ist BT-48 optional, weshalb es nicht auffiel; bei Reverse Charge (§ 13b UStG) und innergemeinschaftlichen Lieferungen ist die Angabe dagegen Pflicht.Invoice::find()liefertc.ust_idjetzt mit, Test 72 prüft BT-31 und BT-48 samt Schemakennung „VA".
2026-07-31 – Firmenlogo persistent in data/logo, Nutzdaten gebündelt unter data/
- Fix #116 hatte die Beleg-Uploads aus dem Web-Root nach
data/uploadsverschoben – inklusive des Unterordnerslogo/. Angepasst wurden damals aber nur Beleg-, Bank- und Invoice-Code;Setting,SettingsControllerundPdfInvoiceServicezeigten weiterhin aufpublic/static/uploads/logo/. Dadurch war das hochgeladene Firmenlogo ab diesem Zeitpunkt nicht mehr auffindbar (Datei vorhanden, Pfad falsch) und ein Neu-Hochladen legte es erneut im Web-Root ab, wo es bei jedem Update verloren ging. - Neue Ablagestruktur: Alle persistenten Nutzdaten liegen jetzt gebündelt und nach Art getrennt unter
data/–data/mariadb(Datenbank),data/belege(Belegdateien),data/logo(Firmenlogo). Jedes Verzeichnis ist ein eigenes Volume;data/mariadbwird nicht mehr in denweb-Container gemountet. data/README.md(neu): Beschreibt für den Admin, was wo liegt, warum die Dateien außerhalb des Web-Roots stehen und wie eine vollständige Datensicherung aussieht.src/Config.php:upload_dirzeigt aufdata/belege, neuer Schlüssellogo_diraufdata/logo.src/Models/Setting.php: Neue MethodelogoAbsolutePath()löst den DB-Wert gegenlogo_dirauf (toleriert Altpfade mitstatic/uploads/-Präfix);deleteLogo()nutzt sie statt des Web-Root-Pfads.setLogo()speichert nur noch den Dateinamen.src/Controllers/SettingsController.php:uploadLogo()schreibt nachdata/logound räumt beim Formatwechsel (png ↔ jpg) die Altdatei ab; neue MethodeserveLogo()liefert das Logo authentifiziert über/settings/logo/fileaus (Pfad- und MIME-Prüfung).src/Services/PdfInvoiceService.php,src/Views/invoices/show.php,src/Views/settings/form.php: Beziehen das Logo über den neuen Endpunkt bzw. den absoluten Dateipfad statt über einen Web-Root-Pfad.migrations/089_pfade_ohne_webroot_praefix.sql(neu): Entfernt die historischenstatic/uploads/-Präfixe ausbelege.storage_pathundcompany_settings.logo_path; idempotent.scripts/migrate-datenverzeichnis.sh(neu): Verschiebt vorhandene Daten aus./uploadsnach./data/belegebzw../data/logo. Der Umzug läuft in einem kurzlebigen Container als root, da die Dateienwww-datagehören – kein sudo nötig, idempotent.docker-compose.yml,Dockerfile,docker-entrypoint.sh,.gitignore: Volumes, Verzeichnisanlage und Rechte auf die neue Struktur umgestellt;.gitignoreschließtdata/*aus (Ausnahme:data/README.md), sodassgit cleandie Nutzdaten nicht mehr treffen kann.- Kein Breaking Change bei bestehenden Installationen: Der Entrypoint übernimmt vorhandene Dateien beim Container-Start automatisch nach
data/belegebzw.data/logo– sowohl aus der gemeinsamenuploads-Ablage (dafür bleibt./uploadsübergangsweise alsdata/legacy-uploadsgemountet) als auch ein Logo aus dem alten Web-Root. Ein reinesscripts/update.sh(git pull + Rebuild + Start) genügt damit; ohne diese Automatik hätte Migration 089 die DB-Pfade umgeschrieben, während die Dateien am alten Ort liegen geblieben wären. - Fix: Datei-Backups liefen seit Fix #116 leer.
scripts/backup.shsicherte weiterhinpublic/static/uploads/– seit dem Umzug aus dem Web-Root ein leeres Verzeichnis. Die Archive enthielten dadurch keine einzige Belegdatei mehr (117 Bytes statt zweistelliger MB-Beträge), ohne dass es auffiel; gesichert wurde nur noch die Datenbank. Das Skript sichert jetztdata/belegeunddata/logound bricht nicht mehr stillschweigend ab, sondern warnt, wenn das Archiv keine Datei enthält. scripts/restore.sh: Stellt Belege und Logo an den neuen Orten wieder her und erkennt zusätzlich das alteuploads/-Archivformat, sodass ältere Datei-Backups einspielbar bleiben.scripts/update.sh: Neuer Schritt 2 aktualisiertscripts/pergit pullund startet sich in der neuen Fassung neu, bevor das Backup läuft. Bislang sicherte ein Update sich noch mit den Skripten des Vorgängerstands ab – dadurch entstand das leere Datei-Backup. Der Ausgangs-Commit wird überSTEUERKRAM_BEFORE_COMMITan den Neustart weitergereicht, damit die Rebuild-Erkennung die Änderungen weiterhin sieht. Der frühere, engere Selbst-Neustart (nur bei Änderung anupdate.shselbst, und erst nach dem Backup) entfällt.scripts/backup.sh: Sichert defensiv jeden Ort, an dem Nutzdateien liegen können (data/belege,data/logo,data/legacy-uploads,data/uploadssowie das alte Web-Root-Verzeichnis), sofern er nicht leer ist. Damit ist das Backup auch im Übergangszustand eines laufenden Updates vollständig, in dem die Skripte schon neu sind, die Dateien aber erst beim nächsten Container-Start umziehen.scripts/restore.sh: Entpackt in ein Staging-Verzeichnis und normalisiert anschließend alle je verwendeten Ablagen (belege/,logo/,legacy-uploads/,uploads/) auf die aktuelle Struktur.tests/71_DatenverzeichnisPfadeTest.php(neu): Hält Backup-, Restore-, Compose-, Entrypoint- und Update-Pfade mitConfig::upload_dir/logo_dirzusammen und prüft, dass die Skript-Vorabaktualisierung vor dem Backup steht – genau dieser Drift hatte das leere Backup verursacht.
2026-07-31 – Fix: Logo-Upload defekt (falsches Ziel-Formular + Warnings in Setting::update())
- Der „Hochladen"-Button in den Einstellungen postete an
/settingsstatt an/settings/logo: Das Logo-Formular lag als verschachteltes<form>innerhalb von#settings-form. HTML erlaubt keine verschachtelten Formulare – der Browser verwirft das innere Start-Tag, wodurch das erste</form>bereits#settings-formschloss. Folge: Der Upload lief nie, stattdessen wurdeSetting::update()mit unvollständigen POST-Daten aufgerufen. Aus demselben Grund war auch der Logo-Löschen-Button (form="delete-logo-form") wirkungslos. Setting::update()lasmail_subject_template/mail_body_templateweiterhin per?:aus$data, obwohl beide Felder seit „E-Mail-Betreff und -Text pro Kunde" (2026-05-30) nicht mehr im Einstellungsformular stehen. Das erzeugte „Undefined array key"-Warnings; deren Ausgabe verhinderte anschließend denLocation-Header inView::redirect()(„headers already sent").src/Views/settings/form.php:#logo-upload-formund#delete-logo-formstehen jetzt als eigenständige, leere Formulare außerhalb von#settings-form; Datei-Input und Buttons sind perform="…"-Attribut daran gebunden.src/Models/Setting.php: Alle optionalen Felder werden konsistent über($data['x'] ?? '') ?: defaultgelesen, sodass fehlende POST-Keys keine Warnings mehr auslösen.tests/70_SettingsLogoUploadTest.php: Neuer Test prüft, dassSetting::update()ohne optionale Keys warnungsfrei durchläuft, dasssetLogo()/deleteLogo()den Pfad korrekt schreiben und dass in#settings-formkein verschachteltes<form>mehr steht.
2026-07-30 – Fix: SMTP-Header-/Kommando-Injection über mail_from_email (Issue #148)
SmtpMailer::sendMessage()/buildMessage()verwendetenfromEmailroh sowohl imMAIL FROM:<...>-SMTP-Kommando als auch imFrom:-Header, während alle anderen dynamischen Werte (Empfänger,fromName,subject, Anhangsname) bereits validiert bzw. CRLF-bereinigt wurden.mail_from_email(Firmeneinstellung, per POST setzbar) durchlief keine serverseitige Prüfung – ein eingeloggter Nutzer konnte per CRLF-Payload zusätzliche SMTP-Kommandos (z.B. eine verdeckteRCPT TO-Zeile) einschleusen oder denFrom:-Header manipulieren (CWE-93).src/Models/Setting.php:Setting::update()validiertmail_from_emailjetzt überfilter_var(..., FILTER_VALIDATE_EMAIL)(neue private MethodesanitizeFromEmail()); ungültige Werte (inkl. CRLF-Payloads) werden verworfen statt gespeichert.src/Services/SmtpMailer.php: Defense-in-Depth – neue MethodesanitizeAddress()entfernt CR/LF ausfromEmail, bevor dieser imMAIL FROM:-Kommando und imFrom:-Header verwendet wird.tests/69_MailFromEmailInjectionTest.php: Neuer Test prüft, dass ungültige/CRLF-behaftete Absenderadressen beim Speichern verworfen werden und dassSmtpMailer::sanitizeAddress()CR/LF zuverlässig entfernt.
2026-07-27 – Fix: Produktions-Härtung der PHP-Fehlerausgabe (Issue #142)
- Das Docker-Image (
php:8.3-apache) übernahm die kompilierten PHP-Defaults, bei denendisplay_errorsaktiviert ist – im offiziellen Image wirdphp.ini-productionnicht automatisch aktiviert. In Kombination mitPDO::ATTR_ERRMODE_EXCEPTION(src/Database.php) und fehlendem globalem Error-Handler hätte jeder unerwartete Fehler (SQL-Fehler, DB-Verbindungsproblem, Typfehler) eine vollständige PHP-Fehlermeldung inkl. Stacktrace, absoluten Serverpfaden und ggf. SQL-Details direkt an den Client ausgegeben (CWE-209, Information Exposure Through an Error Message) – auch für nicht angemeldete Nutzer (/loginist vorAuth::require()erreichbar). php/error-handling.ini(neu):display_errors = Off,display_startup_errors = Off,log_errors = On,error_reporting = E_ALL & ~E_DEPRECATED.Dockerfile: Kopiertphp/error-handling.inizusätzlich zuphp/uploads.ininach/usr/local/etc/php/conf.d/.public/index.php: Registriert ganz zu Beginn (vor Autoload/Routing)set_error_handler(wandelt Warnings/Notices inErrorExceptionum),set_exception_handlerundregister_shutdown_function(fängt Fatal Errors, die nicht als Exception auftreten). Unerwartete Fehler werden pererror_logserverseitig geloggt; der Client erhält nur eine generische HTTP-500-Seite ohne Details.tests/68_ProductionErrorHardeningTest.php: Neuer Test prüft die Ini-Direktiven, die Dockerfile-Einbindung, die Registrierung der Handler vor dem Autoload sowie dass die an den Client gesendete Fehlerseite keine Exception-Details (Message/Trace/File) enthält.
2026-07-27 – Fix: Stored XSS über XSLT in hochgeladenen XML-Belegen (Issue #135)
BelegController::serveFile()liefert Belege mit dem in der DB hinterlegtenmime_typeundContent-Disposition: inlineunverändert an den Browser aus. Da für Belegeapplication/xml/text/xmlals MIME-Typ erlaubt ist, konnte eine hochgeladene XML-Datei mit eingebettetem<?xml-stylesheet?>/xsl:stylesheetbeim Öffnen über den „Datei öffnen“-Link (target="_blank", keindownload-Attribut) direkt im Browser (v.a. Firefox) per XSLT-Transformation zu ausführbarem<script>im Sicherheitskontext der Anwendung führen (Stored-XSS, CWE-79) – inklusive Zugriff auf das im<meta>-Tag liegende CSRF-Token (vgl. #132/#133).src/Controllers/BelegController.php:serveFile()liefert XML-Belege (application/xml/text/xml) jetzt mitContent-Disposition: attachmentstattinlineaus, sodass der Browser die Datei herunterlädt statt sie zu navigieren/rendern; andere MIME-Typen bleiben unverändertinline.public/index.php: GlobalerX-Content-Type-Options: nosniff-Header für alle Responses als zusätzliche Absicherung gegen MIME-Sniffing.tests/67_BelegXmlXsltXssTest.php: Neuer Test lädt einen XML-Beleg mit XSLT-Payload an und prüft, dassserveFile()denContent-Disposition-Header aufattachmentstattinlinesetzt.
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