mahnung/CHANGELOG.md
Eduard Wisch 0244a5b07e feat(mahnung): Mailversand ueber FormMail, Versandprotokoll, HTML-Mails
Phase 13/14 abgeschlossen. Der Mailversand war zuvor KAPUTT: ajax/sendmail.php
war bereits zur Funktionsbibliothek umgebaut, card.php postete aber weiterhin
per JS dagegen.

Mailformular (FormMail)
- card.php nutzt jetzt Dolibarrs Standard-Mailformular (action=presend/send):
  Empfaenger (Firma + alle Ansprechpartner), Betreff, Text und Anhang sichtbar
  und aenderbar. Gesendet wird ausschliesslich ueber mahnungSendeErinnerungsMail().
- Anhang = unveraenderte Original-Rechnungs-PDF, wird bei Bedarf nacherzeugt.
  Eigener Parameter mailinit statt mode=init, weil get_form() bei mode=init die
  Anhangsliste selbst leert.
- HTML-Mails: DolEditor im Setup + withfckeditor=-1 im Formular (folgt
  FCKEDITOR_ENABLE_MAIL wie Dolibarrs eigene Mailvorlagen).
- Klartext bleibt Klartext: GETPOST('restricthtml') jagt jeden Nicht-HTML-Text
  durch dol_nl2br() — mahnungBodyEntkleiden() nimmt nur dieses Artefakt zurueck
  und laesst echte Formatierung unangetastet.
- Platzhalter jetzt auch in Dolibarr-Schreibweise (__REF__, __DATE_YMD__,
  __AMOUNT_FORMATED__, __DATE_DUE_YMD__, __FRIST_TAGE__ ...), Liste sichtbar im Setup.
- Absender-Adresse und -Name konfigurierbar (MAHNUNG_EMAIL_SENDER[_NAME]).
- Erneuter Versand moeglich (force aus dem Status abgeleitet, nicht aus dem
  Request — Doppelversand-Schutz bleibt wirksam).

Versandprotokoll (neue Tabelle llx_mahnung_mailprotokoll)
- Jede versendete Erinnerung wird mit Empfaenger, Kopie, Betreff, Text und
  Anhangsnamen festgehalten, einsehbar unter Versandstatus. Historie statt
  Spalten am Vorgang, weil erneut gesendet werden kann.
- Lazy-Migration legt die Tabelle an (DB_VERSION 0.4.0), kein Reaktivieren noetig.

Haertung nach Code-Review (21 bestaetigte Funde)
- Anhang liess sich nicht abwaehlen (wurde sofort wieder eingehaengt)
- Upload/Entfernen ohne Rechtepruefung; Temp-Verzeichnis pro Vorgang getrennt
- Teilzahlung zwischen Oeffnen und Senden fuehrt zurueck ins Formular
- Empfaenger: Semikolon-Trenner, keine stillen Verwerfungen, Dubletten, CR/LF
- CSRF: presend + Core-Dateiaktionen (confirm_deletefile, renamefile, sendit,
  linkit) token-pflichtig
- Externe Benutzer sehen nur eigene Vorgaenge; Abschreiben verlangt facture.creer

UI
- Mahnstufe nur noch EINE Darstellung (Badge), Farbskala zentral in
  lib/mahnung_ui.lib.php statt doppelt gepflegt
- Zahnrad oben rechts in die Einstellungen (nur mit Recht mahnung.setup)
- Original-Rechnung unter Verknuepfte Dokumente mit Vorschau, Groesse in KB
- PDF-Einleitungstext nur noch, wo ueberhaupt ein PDF entsteht

Sprachdateien de_DE/en_US deckungsgleich, 9 tote Keys entfernt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:31:25 +02:00

31 KiB
Raw Blame History

Changelog

[Unreleased]

Zahlungserinnerung (Stufe 0) + frei konfigurierbare Mahnstufen

  • Vor der ersten echten Mahnung steht jetzt eine kostenlose Zahlungserinnerung. Ob eine Stufe eine Erinnerung ist, entscheidet ausschließlich das neue Flag llx_mahnung_stufe.ist_erinnerungnicht die Stufennummer. Eine Erinnerung kostet nichts: Mahngebühr, §288-Abs.-5-Pauschale und Verzugszinsen werden in mahnungBaueVorgang() hart auf 0 gesetzt, unabhängig davon, was in der Stufe konfiguriert ist.
  • Keine feste Obergrenze bei Stufe 3 mehr. Stufen sind frei konfigurierbar (Nummer 0127, Lücken erlaubt). Einstieg ist die kleinste aktive Stufe (minStufe()), die Folgestufe liefert naechsteStufeNach() — Lücken werden übersprungen. Auch der Button „Als uneinbringlich klassifizieren" hängt nicht mehr an stufe === 3, sondern daran, dass keine weitere aktive Stufe mehr folgt.
  • Wartefrist korrigiert: maßgeblich ist frist_tage der Zielstufe, nicht neue_frist_tage der Vorstufe. Vorher war frist_tage der Stufen 2/3 ein toter Konfigurationswert. Bezugspunkt ist date_versand, Fallback date_mahnung (Bestandsdaten haben kein Versanddatum).
  • Neue Spalte llx_mahnung_mahnung.kosten_vorstufen: die Gebühren und Pauschalen bereits gemahnter Vorstufen fielen bisher aus der Forderung heraus, weil rechneSumme() nur die aktuelle Stufe addierte.
  • Zinssatz-Spalten von DECIMAL(5,4) auf DECIMAL(6,3): 5,4 konnte maximal 9,9999 abbilden — der B2B-Standardsatz (Basiszins + 9 %) passte damit gar nicht hinein.
  • modMahnung::ensureSchema() zieht das Schema nach einem reinen Datei-Deploy nach (Aufruf am Kopf von list.php, card.php und den AJAX-Endpoints). Ohne das schlug nach jedem Deploy jedes create() fehl, bis jemand die Setup-Seite aufgerufen hatte.
  • Label-Vereindeutigung läuft genau einmal (Marker MAHNUNG_LABEL_MIGR_DONE) und trifft nur unveränderte Seed-Texte: Stufe 2 „1. Mahnung" → „2. Mahnung", danach Stufe 1 „Zahlungserinnerung" → „1. Mahnung". Die Reihenfolge ist zwingend, sonst hieße „1. Mahnung" doppelt. Bestandsdaten (Mahnvorgänge, Beträge) bleiben unangetastet.

E-Mail-Versand über das Dolibarr-Mailformular (FormMail)

  • Der Versand der Zahlungserinnerung läuft jetzt über das Standard-Mailformular auf der Mahnungskarte (card.php?id=…&action=presend). Empfänger, Betreff, Text und Anhang sind vor dem Absenden sichtbar und änderbar — vorher ging die Mail nach einem bloßen Ja/Nein-Dialog ungesehen raus.
    • Empfängerauswahl enthält die Firmenadresse und alle aktiven Ansprechpartner des Kunden (thirdparty_and_contact_email_array(1)), dazu ein Freitextfeld und ein CC-Feld. Aufgelöst wird ein Empfänger nur, wenn sein Schlüssel wirklich aus der Liste dieses Kunden stammt: Societe::contact_get_property() fragt allein über die rowid ab und prüft die Firmenzugehörigkeit nicht — ein manipuliertes receiver[] hätte die Erinnerung sonst an einen fremden Kontakt schicken können.
    • Absender ist die Firmenadresse, nicht der angemeldete Benutzer — eine Zahlungserinnerung geht im Namen des Betriebs raus. Sie wird nur angezeigt, nicht zur Auswahl gestellt; ein trotzdem geposteter Wert wird ignoriert.
    • Angehängt wird die unveränderte Original-Rechnungs-PDF, vorbelegt beim Öffnen des Formulars (Parameter mailinit=1) und bei Bedarf über Facture::generateDocument() nacherzeugt. Weitere Dateien lassen sich anhängen und wieder entfernen. Bewusst nicht Dolibarrs mode=init: FormMail::get_form() leert bei diesem Parameter selbst die Anhangsliste und hätte die eingehängte Rechnung sofort wieder entfernt.
    • Betreff und Text kommen aus der Stufen-Konfiguration mit bereits ersetzten Platzhaltern ({rechnung}, {summe}, {frist}, {kunde}, {ref}, {stufe}) — im Formular stehen keine {…}-Marken mehr. Nachträglich ergänzte Platzhalter werden beim Absenden erneut aufgelöst.
  • ajax/sendmail.php ist kein Endpoint mehr, sondern eine Funktionsbibliothek. Es gibt genau einen Sendeweg (mahnungSendeErinnerungsMail()); zwei nebeneinander hätten auseinanderlaufen können. Ein Direktaufruf der Datei per HTTP verschickt nichts, sondern leitet auf das Formular um (und antwortet einem alten jQuery-Aufruf sauber mit JSON statt mit HTML).
  • Doppelversand ist blockiert: der Vorgang wird vor dem Senden per bedingtem UPDATE atomar auf VERSENDET reserviert — senden darf nur der Request, dessen UPDATE tatsächlich eine Zeile trifft. Schlägt der Versand fehl, wird ausschließlich die eigene Reservierung wieder zurückgenommen.
  • Beträge und Frist werden zum Versandzeitpunkt frisch geprüft: ist die Rechnung inzwischen bezahlt, storniert oder abgeschrieben, wird der Versand abgelehnt; bei einer Teilzahlung werden betrag_offen/summe_mahnung nachgezogen, damit Mail und Datensatz denselben Betrag nennen. Die neue Zahlungsfrist wird aus neue_frist_tage neu gerechnet — sonst nennt eine später abgeschickte Erinnerung eine bereits verstrichene Frist.
  • Fachliche Sperre bleibt: versendbar ist ausschließlich eine Stufe mit ist_erinnerung = 1. Echte Mahnungen gehen per Post bzw. Einschreiben raus, weil eine E-Mail nicht beweisbar zugestellt ist.

Versandprotokoll: was ging wann an wen raus

  • Jede versendete Zahlungserinnerung wird protokolliert — Empfänger, Kopie, Blindkopie, Absender, Betreff, der komplette Text und die Namen der Anhänge, jeweils im Stand des Sendezeitpunkts. Ändert jemand später die Vorlage in der Stufen-Konfiguration, bleibt der Nachweis davon unberührt.
  • Einsehbar auf der Mahnungskarte unter Versandstatus → „Versendete E-Mails": zugeklappt stehen Zeitpunkt und Empfänger, aufgeklappt der vollständige Mailinhalt. HTML-Mails werden formatiert dargestellt (durch dol_string_onlythesehtmltags(), da der Inhalt im Browser ausgegeben wird), Klartext mit erhaltenen Zeilenumbrüchen.
  • Eigene Tabelle llx_mahnung_mailprotokoll statt Spalten am Mahnvorgang: seit dem Wiederholversand kann dieselbe Erinnerung mehrfach rausgehen — Spalten am Vorgang würden den vorherigen Versand überschreiben, also genau den Nachweis zerstören, um den es geht. Jeder Versand ist ein eigener Eintrag, neueste zuerst.
  • Geschrieben wird ausschließlich nach erfolgreichem Versand, direkt in mahnungSendeErinnerungsMail() — damit kann kein Sendeweg das Protokollieren umgehen. Scheitert das Protokollieren selbst, wird das nur ins Syslog geschrieben und der Versand nicht nachträglich als gescheitert gemeldet.
  • Die Tabelle entsteht auf Bestandsinstallationen über die Lazy-Migration (ensureSchema()ensureMailProtokollTabelle()), DB_VERSION steht jetzt auf 0.4.0. Ein reiner Datei-Deploy genügt, es braucht keine Modul-Neuaktivierung.

Setup: PDF-Einleitungstext nur noch, wo es ein PDF gibt

  • Bei Versandart E-Mail und bei jeder Zahlungserinnerung stand im Block „Texte für das Schreiben" weiterhin der PDF-Einleitungstext — dabei entsteht in beiden Fällen gar kein Mahnschreiben, in das er könnte (eine Erinnerung hat nie ein eigenes PDF, angehängt wird die Original-Rechnung). Das Feld wird jetzt spiegelbildlich zu den E-Mail-Feldern ein- und ausgeblendet, auch beim Umschalten der Versandart ohne Neuladen (syncEmailRows()).
  • Bewusst ausgeblendet statt entfernt: ein bereits gepflegter Einleitungstext bleibt im Formular und damit erhalten, falls die Stufe später wieder auf Postversand umgestellt wird.

Zahnrad in die Einstellungen

  • Vorschlagsliste, Archiv und Mahnungskarte haben oben rechts neben dem Titel ein Zahnrad, das direkt in die Modul-Einstellungen führt (mahnungSetupLink() in lib/mahnung_ui.lib.php, gesetzt über den morehtmlright-Parameter von load_fiche_titre()). Sichtbar nur mit dem Recht mahnung.setup oder für Administratoren — wer die Seite ohnehin nicht öffnen darf, sieht kein Icon, das ihn in eine Fehlermeldung laufen lässt.

HTML-Mails, Wiederholversand, Rechnung in der Dokumentenliste

  • Mailtexte lassen sich als HTML pflegen — mit dem gewohnten Dolibarr-Editor. Im Setup hing bisher ein nacktes <textarea> am Stufen-Mailtext; jetzt steht dort DolEditor mit derselben Toolbar und derselben Konstante (FCKEDITOR_ENABLE_MAIL) wie bei Dolibarrs eigenen Mailvorlagen. Das Mailformular auf der Karte nutzt withfckeditor = -1, entscheidet also ebenfalls nach der globalen Einstellung. Ist der WYSIWYG abgeschaltet, bleibt alles beim einfachen Textfeld.
    • mahnungBodyEntkleiden() greift deshalb nur noch, wenn der Text außer <br> keine Tags enthält: das nl2br-Artefakt wird zurückgebaut, echte Formatierung (fett, Listen, Links) bleibt unangetastet.
  • Eine Zahlungserinnerung lässt sich erneut versenden. Vorher verschwand der Button nach dem ersten Versand — kam die Mail nicht an, landete im Spam oder ging an die falsche Adresse, gab es keinen Weg zurück. Der force-Pfad der Bibliothek ist jetzt verdrahtet: der Button heißt dann „Zahlungserinnerung erneut senden", das Formular warnt mit dem Datum des letzten Versands, und der Empfänger lässt sich vorher ändern. force wird aus dem Status abgeleitet, nicht aus einem Request-Parameter — der Doppelversand-Schutz greift weiter (die Reservierung nagelt zusätzlich das bisherige Versanddatum fest). Erledigte (bezahlte) und stornierte Vorgänge bleiben gesperrt.
    • Versendbar ist ein Vorgang außerdem schon dann, wenn irgendein Empfänger existiert — auch nur ein Ansprechpartner mit Mailadresse; im Formular lässt sich ohnehin eine beliebige Adresse eintragen.
  • Die Original-Rechnung steht jetzt unter „Verknüpfte Dokumente" — mit Größe, Datum, Download und Vorschau-Lupe, gekennzeichnet mit „geht als Anhang mit". Vorher meldete die Sektion bei einer Erinnerung „Keine Dokumente vorhanden", und zum Nachschauen musste man auf die Rechnungskarte wechseln. Gelistet wird nur, was schon existiert (mahnungFindeRechnungsPdf()) — das bloße Ansehen der Karte erzeugt bewusst kein PDF; fehlt es noch, sagt ein Hinweis, dass es beim Öffnen des Mailformulars entsteht.

Härtung nach Code-Review des Mailformulars

  • Klartext bleibt Klartext. GETPOST(…, 'restricthtml') schickt jeden Nicht-HTML-Text durch dol_nl2br() — allein durch das Abschicken des Formulars wurde aus dem Mailtext HTML mit <br>-Tags. Sichtbar wurde das doppelt: die Tags standen im Textfeld, und dol_textishtml() stufte den Text danach als HTML ein, sodass eine als Klartext gepflegte Erinnerung als HTML-Mail rausgegangen wäre. Neue Funktion mahnungBodyEntkleiden() macht das rückgängig, solange die Stufen-Vorlage Klartext ist.
  • Der Anhang ließ sich nicht abwählen: die Vorbelegung hing an „Liste ist leer" und hängte die Rechnungs-PDF nach dem Entfernen im selben Request sofort wieder ein. Sie hängt jetzt ausschließlich am Parameter mailinit plus einem Session-Marker.
  • Upload/Entfernen von Anhängen prüft jetzt die Berechtigung ($canSendErinnerung). Vorher konnte jeder eingeloggte Benutzer mit gültigem Token Dateien in sein Temp-Verzeichnis laden, obwohl er nichts versenden darf. Das Temp-Verzeichnis ist zusätzlich pro Mahnvorgang getrennt — dol_add_file_process() speichert mit allowoverwrite, gleichnamige Anhänge zweier Vorgänge hätten sich sonst überschrieben.
  • Teilzahlung zwischen Öffnen und Absenden: Betreff und Text stehen mit bereits ersetzten Platzhaltern im Formular, ein nachgezogener Betrag wäre also nicht mehr in den Text gekommen. Statt eine Mail mit falschem Betrag zu verschicken, führt der Fall zurück ins Formular, das mit dem aktuellen Betrag neu aufgebaut wird (MahnungMailBetragAngepasst).
  • Empfänger: Freitextfelder trennen jetzt auch am Semikolon (Outlook-Gewohnheit), unbrauchbare Einträge werden nicht mehr still verworfen sondern melden sich, Dubletten zwischen Auswahlliste und Freitext fallen zusammen (Schlüssel ist die kleingeschriebene Adresse), und wer im An-Feld steht, bekommt keine zusätzliche Kopie. Firmen- und Absendername laufen durch mahnungHeaderEinzeilig()dol_string_nospecial() allein lässt CR/LF stehen.
  • CSRF: presend ist token-pflichtig (die Action stellt die Rechnungs-PDF bereit und erzeugt sie notfalls neu), ebenso die Core-Dateiaktionen aus core/actions_linkedfiles.inc.php (confirm_deletefile, confirm_updateline, renamefile sowie die parameterbasierten sendit/linkit/renamefilesave). Der Core-CSRF-Check ist auf dieser Installation abgeschaltet; ohne die Ergänzung hätte ein untergeschobener GET-Aufruf einen hochgeladenen Zustellbeleg löschen können.
  • Externe Benutzer (Kundenkontakte mit eigenem Zugang) sehen nur noch Mahnvorgänge ihrer eigenen Firma — Mahnung::fetch() filtert bloß nach Entity.
  • Abschreiben als uneinbringlich verlangt zusätzlich das Recht „Rechnungen anlegen/ändern"; der Schritt schreibt auf der Rechnung und ist praktisch unwiderruflich. Ohne das Recht erscheint der Button nicht mehr.
  • Neun tote Lang-Keys des alten Versandwegs entfernt; die Sprachdateien sind deckungsgleich und vollständig.

Setup-Seite als Akkordeon

  • Die Stufen-Konfiguration ist in aufklappbare Blöcke gegliedert (Fristen / Kosten / Versand / Zinssatz-Override / Texte). Alle Stufen starten zugeklappt; die Kopfzeile fasst das Wesentliche zusammen (kostenlos, Auslösefrist, Gebühr B2C/B2B, inaktiv, nicht löschbar).

Darstellung der Mahnstufe vereinheitlicht

  • Die Mahnstufe stand auf der Karte und in der Vorschlagsliste doppelt da: als farbiger Badge („Erinnerung" / „Zahlungserinnerung") und direkt daneben nochmal als Text („0 — Zahlungserinnerung"). Jetzt gibt es genau eine Darstellung — Nummer und Bezeichnung stehen zusammen im Badge, die Art der Stufe transportieren Farbe und Tooltip.
  • Neue lib/mahnung_ui.lib.php mit mahnungStufeFarbe() und mahnungStufeBadge(). Die Farbskala war vorher in list.php und core/boxes/box_mahnung_offen.php doppelt gepflegt (mit dem Kommentar „bewusst identisch") und wäre beim nächsten Eingriff auseinandergelaufen; jetzt hat sie eine Quelle. Das Widget behält seine kompakte Beschriftung („Stufe N"), weil die Spalte dort schmal ist.

Widget (box_mahnung_offen)

  • Neue Spalte „Vsl. Zahlung" (Zahlungsprognose): Je offener Rechnung wird prognostiziert, wann der Kunde sie voraussichtlich zahlt — als konkretes Datum plus der direkt lesbaren Kennzahl „Ø X T nach Rechnung" (Days-to-Pay). Zweizeilige Zelle: Ampel-Icon + Prognosedatum / darunter Ø <Tage> nach Rechnung · n=<Stichprobe>.
    • Prognose am Rechnungsdatum verankert: Prognosedatum = datef + Ø(Zahlung Rechnungsdatum). Das ist die intuitive „zahlt X Tage nach Rechnungseingang"-Zahl (positiv, kein Kopfrechnen wie bei einer Differenz zur Fälligkeit) und verhindert Prognosen vor dem Rechnungsdatum. Fallback auf Fälligkeit + diff, falls datef fehlt.
    • Ampel-Skala 1:1 aus dem BuchhaltungsWidget übernommen (KB #886), damit das Icon zur „Zahlungsverhalten"-Box auf der Kundenkarte passt: Bewertung über diff = Ø Tage nach Fälligkeit; Stufen ≤5 Vorbildlich ★ / ≤0 Pünktlich ✓ / ≤7 Spätzahler ⚠ / ≤14 Verspätet ⏱ / >14 Problematisch ✗ (Farben #28a745/#ffc107/#fd7e14/#dc3545). Icon = Pünktlichkeit (relativ zur Fälligkeit), sichtbare Zahl = Tage nach Rechnung — beides wie auf der Kundenkarte.
    • Berechnung als self-contained Kopie (getZahlprognose(), 1 Query je Kunde/Request gecacht) — keine Laufzeit-Abhängigkeit auf das BuchhaltungsWidget. Filter identisch KB #886: type IN (0,1,5), fk_statut=2 + paye=1, date_lim_reglement IS NOT NULL, DATEDIFF, entity.
    • Verstrichene Prognose (Rechnung noch offen, zahlt langsamer als sein Muster) → das Prognosedatum wird rot eingefärbt statt mit Zusatztext beschriftet (hält die Widget-Spalte schmal); die Erläuterung („später als üblich") steht im Tooltip.
    • Mindest-Stichprobe via Konstante MAHNUNG_PROGNOSE_MIN_N (Default 1 = Parität zur Kundenkarten-Box, die ebenfalls ab der 1. bezahlten Rechnung bewertet), darunter „keine Historie". Neue Lang-Keys MahnungProg* (de_DE + en_US).
  • Summenzeile Netto + Brutto: Die liste_total-Zeile zeigt jetzt zweizeilig Netto (SUM(f.total_ht)) und Brutto (SUM(f.total_ttc)). Der Bruttobetrag kommt direkt aus f.total_ttc der Rechnung und wird nicht aus dem Netto hochgerechnet — so bleiben Steuer-Sonderregelungen (Reverse-Charge §13b, Steuerbefreiung, Kleinunternehmer §19 UStG) korrekt. Einzelzeilen zeigen weiterhin nur Netto. Neue Lang-Keys MahnungBoxNetto/MahnungBoxBrutto (de_DE + en_US).
  • Zähler im Kopf: Der Badge im Widget-Kopf zeigt die tatsächliche Gesamtzahl offener Rechnungen (verlinkt auf die gefilterte Rechnungsliste). Titel-Lang-Key ohne (%s).
  • Zeilenanzahl konfigurierbar via Konstante MAHNUNG_BOX_MAXLINES (Admin-Select in setup.php: Alle/5/10/20/30/50, Default 0 = alle). Widget lädt immer alle offenen Rechnungen (korrekter Zähler), rendert aber nur MAHNUNG_BOX_MAXLINES Zeilen + eine ...-Überlaufzeile.

Schema-Fix (tms-Spalten)

  • Die tms-Spalten von llx_mahnung_mahnung, llx_mahnung_stufe und llx_mahnung_trackingpattern wurden als reines TIMESTAMP angelegt — unter explicit_defaults_for_timestamp entstand daraus NULL DEFAULT NULL, sodass tms bei jedem UPDATE leer blieb. Jetzt Dolibarr-Standard DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, damit der Änderungszeitpunkt (z.B. einer per Zahlungstrigger erledigten Mahnung) wieder nachvollziehbar ist.
  • Neue idempotente Migration migrateTimestampSpalten() (läuft im Modul-init()): befüllt bestehende NULL-Werte aus datec und stellt die Spalte per ALTER TABLE um. Greift auf Bestands-Installs beim Re-Aktivieren des Moduls.

Bonitaet / Forderungsausfall-Workflow

  • Neuer Hook tabContentViewThirdparty rendert auf der Kundenkarte eine prominente rote Warnbox, wenn der Kunde abgeschriebene Rechnungen (fk_statut=3 + close_code='badcustomer') hat. Zeigt Anzahl, Gesamtsumme, Datum der letzten Abschreibung + Link zur Detail-Liste.
  • Neuer Hook formObjectOptions zeigt eine kompakte Warn-Zeile bei Auftrags-/Rechnungs-Karten ("ordercard", "invoicecard"), wenn der Kunde Forderungsausfaelle hat — Bonitaets-Pruefung vor neuem Geschaeft.
  • Neuer Hook-Kontext ordercard zum Modul-Descriptor ergaenzt.
  • Neuer Button "Als uneinbringlich klassifizieren" auf Mahnung-Karten der Stufe 3 (Status ≥ ERSTELLT, nicht storniert, Rechnung nicht bereits abandoned).
    • Bestaetigungs-Dialog mit Begruendungs-Textfeld (Default: "Mahnverfahren erfolglos abgeschlossen am ...")
    • Ruft Facture::setCanceled($user, CommonInvoice::CLOSECODE_BADDEBT, $note) → Rechnung auf fk_statut=3 + close_code='badcustomer'
    • Mahnung wird zugleich storniert und die Begruendung in note_private festgehalten
  • Steuerlich passt das: Dolibarrs Steuer-Modul (EÜR) ignoriert abandoned Rechnungen automatisch (UStVA filtert fk_statut IN (1,2); EÜR liest nur llx_paiement = tatsaechliche Zahlungen). Bei Ist-Versteuerung ist damit alles korrekt — keine separate Buchung noetig.

UX-Fixes (Vorschlagsliste)

  • Kundentyp-Filter (B2B/B2C) wird jetzt direkt an select_company() durchgereicht — wenn B2C gewaehlt ist, zeigt das Kunden-Dropdown nur noch Drittparteien ohne TVA-Nummer (entsprechend umgekehrt fuer B2B). Filter nutzt Dolibarrs Universal-Search-Criteria-Syntax (feld:operator:wert) — plain SQL wuerde durch forgeSQLFromUniversalSearchCriteria fehlschlagen.
  • Auto-Submit beim Wechsel des Kundentyps + automatisches Reset von search_socid, damit das Dropdown ohne extra "Suche"-Klick aktualisiert wird und keine ungueltige (im neuen Filter nicht enthaltene) Kunden-ID stehen bleibt.
  • search_socid=-1 (von select_company als "nichts ausgewaehlt" geliefert) wird jetzt korrekt ignoriert statt als Filter auf fk_soc=-1 zu wirken.
  • Skip-Grund-Spalte in der Uebersprungen-Tabelle: opacitymedium jetzt am inneren span statt am td, damit Theme-spezifisches Zellen-Border-Verhalten konsistent bleibt.

Versand-Reminder (Cron + Ntfy)

  • Neuer Cron-Job MahnungCronVersandReminder (taeglich): sucht Mahnungen mit Status ERSTELLT deren PDF seit > N Tagen erstellt aber noch nicht versendet wurde, schickt Ntfy-Push und (falls aktiv) GlobalNotify-Badge.
  • Schwellenwert konfigurierbar via Konstante MAHNUNG_VERSAND_REMINDER_DAYS (Default 2).
  • Nachricht listet bis zu 8 Mahnungen (Ref + Stufe + Alter in Tagen + Kunde); Rest als "+N weitere".

Beleg-Scan mit Sendungsnummer-Erkennung

  • Neuer Button "Belege scannen" im Versand-Block der Mahnungs-Karte.
  • Beim Klick werden alle hochgeladenen Belege (PDF via pdftotext, sonst txt/html) durchsucht und gegen die konfigurierten Tracking-Patterns gematcht.
  • OCR-Fallback: wenn pdftotext nur Form-Feed-Zeichen liefert (Bild-PDF), wird automatisch ocrmypdf --skip-text -l deu+eng aufgerufen und das OCR-PDF erneut mit pdftotext gelesen. Temporaeres OCR-PDF wird nach Extraktion geloescht.
  • Erkannte Sendungsnummern werden als Vorschlag ueber dem Beleg-Bereich angezeigt (mit Dateiname, Provider-Label, Sendungsnummer + Deep-Link).
  • Per "Uebernehmen"-Button werden tracking_nr + tracking_provider gespeichert; zusaetzlich werden date_versand und versandweg automatisch gesetzt (falls noch leer), sodass kein separates Speichern noetig ist.
  • "Verwerfen" entfernt den Vorschlag aus der Session.
  • DPAG-Einschreiben-Regex erlaubt OCR-typische Leerzeichen zwischen Zifferngruppen (R[A-Z]\s?\d{4}\s?\d{4}\s?\d\s?DE).

Konfigurierbare Tracking-Patterns (Setup-Seite)

  • Neue Tabelle llx_mahnung_trackingpattern (Pro Eintrag: provider, label, regex, url_template, priority, active). Auto-Migration + Default-Seed beim Setup-Aufruf.
  • Default-Patterns: DHL Paket (20-stellig), DPAG Einschreiben (RR123456789DE), UPS (1Z…), DHL 11-stellig, Hermes 14-stellig, DPD 14-stellig — Prioritaeten so gesetzt dass spezifischere Patterns zuerst greifen.
  • Neue Setup-Seite admin/tracking_patterns.php mit CRUD: Pattern anlegen/bearbeiten/aktivieren-deaktivieren/loeschen.
  • Live-Vorschau: Beim Tippen von Regex/URL/Beispieltext wird via AJAX-Endpoint ajax/regex_preview.php direkt gezeigt ob der Regex syntaktisch gueltig ist, was er aus dem Beispieltext matcht und wie die finale Tracking-URL aussieht.
  • ReDoS-Schutz im AJAX-Endpoint: max 10 KB Sample, pcre.backtrack_limit=100k, Whitelist Delimiter / # ~.
  • Mahnung::trackingUrl() (hardcoded Fallback) bleibt — primaer wird MahnungTrackingPattern::urlFor() aus DB-Patterns benutzt.
  • Setup-Page-Link: Button "Tracking-Muster (Regex)" oben rechts auf der Modul-Setup-Seite.

Versand & Belege (Mahnungs-Karte)

  • Neue Felder date_versand, versandweg, tracking_nr, tracking_provider an llx_mahnung_mahnung — idempotente Migration laeuft beim ersten Setup-Aufruf nach dem Deploy.
  • Neuer Block "Versand & Belege" auf der Mahnungs-Karte:
    • Erfassung Versanddatum + Versandweg (Brief/Einschreiben/DHL/DPD/Hermes/UPS/Fax/Mail/Persoenlich/Eigen).
    • Optionale Sendungsnummer + Anbieter — Mahnung-Klasse liefert Deep-Link zur Sendungsverfolgung (DHL, Deutsche Post, DPD, Hermes, UPS).
    • "Sendung verfolgen"-Button oeffnet die Provider-Seite mit eingesetzter Sendungsnummer.
  • Beleg-Upload via Dolibarrs formfile->showdocuments() — Dateien landen in DOL_DATA_ROOT/mahnung/<MAHN-Ref>/, voll integriert mit ECM/document.php.
  • Status springt automatisch auf STATUS_VERSENDET sobald ein Versanddatum gesetzt wird (sofern vorher <= ERSTELLT).
  • Neue Methoden Mahnung::setVersand(), Mahnung::trackingUrl(), Mahnung::defaultProviderForWeg(), Mahnung::getVersandwegLabel().

Vorschlagsliste — UX

  • Kunden-Filter: rowid-Input ersetzt durch Dolibarr-Standard select_company() (Ajax-Suche bzw. klassisches Dropdown, je nach Dolibarr-Konfiguration). Direkt-Links ?search_socid=74 bleiben funktional.
  • Neuer Filter "Mindestbetrag" (in EUR, Komma erlaubt).
  • Neuer Filter "Kundentyp" (B2B / B2C).
  • Neue Spalte "Kontakt" mit Telefon- und Mail-Direktlink-Icons.

Verzugszinsen-Neuberechnung

  • Mahnungen im Status "Erstellt" (noch nicht versandt) werden beim Aufruf der card.php automatisch mit der aktuellen Stufen-Konfiguration neu berechnet. Aenderungen am Zinssatz-Override in den Einstellungen wirken sofort auf alle offenen Mahnungen.
  • Toleranz 0,001 EUR um unnoetige DB-Writes zu vermeiden.
  • basiszins_snapshot wird ebenfalls aktualisiert.

Zinssatz-Override UX (Setup-Seite)

  • Zinssatz-Override-Felder zeigen jetzt einen Placeholder mit dem Standard-Zinssatz (z.B. "6,27").
  • Grauer Hilfetext neben dem Feld: "Leer = Standard (1,27 + 5,0 % = 6,27 %), 0 = keine Zinsen" — macht die Unterscheidung zwischen leer (Standard) und 0 (keine Zinsen) klar.
  • Effektiver Zinssatz wird live aus den globalen Basiszins-/Aufschlag-Einstellungen berechnet.

Fixes

  • Deutsche Post Tracking-URL korrigiert: ?piececode={nr} statt ?form.sendungsnummer={nr}.
  • showdocuments() Return-Wert wurde nicht geprinted — Sendebelege waren nach Upload unsichtbar.
  • $upload_dir war im scan_belege-Handler undefiniert (Variable wurde erst 90 Zeilen spaeter gesetzt) — eigene Pfadberechnung im Action-Block.
  • pdftotext Form-Feed-Zeichen (\x0C) bei Bild-PDFs: trim() entfernt \x0C nicht — explizite Zeichenliste noetig.
  • Kundenkarte: Tab "Mahnwesen" erschien doppelt, weil complete_head_from_modules() pro Karte mehrfach (core + external + remove) feuert. Hook filtert jetzt auf mode=add + filterorigmodule=external.

[0.2.0] — 2026-05-10 — ODT-Template-System, Widget, Dokumentenmodelle

ODT-Template-System

  • Abstrakte Basis-Klasse ModelePDFMahnung extends CommonDocGenerator (core/modules/mahnung/modules_mahnung.php)
  • ODT-Generator doc_generic_mahnung_odt mit Stufen-spezifischer Template-Auswahl (mahnung_stufe1/2/3.odt, Fallback mahnung.odt)
  • TCPDF-Generator pdf_standard_mahnung (refactored aus mahnungpdf.class.php)
  • mahnung.class.php: generateDocument() Methode via commonGenerateDocument()
  • Template-Variablen: Mahnung, Rechnung, Kunde, Bankverbindung, Dolibarr-Standard
  • ODT-Template-Upload auf Setup-Seite mit Benennungskonvention-Hinweis

Widget

  • box_mahnung_offen: Offene Kundenrechnungen mit Mahnstufe-Badge (basiert auf box_factures_imp)
  • Alle offenen Rechnungen (nicht nur überfällige), Status-Icon wie Original
  • Farbige Mahnstufe-Badges (blau/orange/rot) mit Link zur Mahnung-Detailseite
  • Strich (—) bei Rechnungen ohne Mahnung

Dokumentenliste auf card.php

  • Generierte Dokumente zur Mahnung auflisten (aus Rechnungsordner)
  • PDF-Vorschau (Lupe), Download-Button
  • Modellauswahl-Dropdown bei mehreren aktiven Dokumentenmodellen

Setup-Erweiterungen

  • Dokumentenmodell-Verwaltung (aktivieren/deaktivieren, Default setzen)
  • admin/templatevars.php: Referenzseite aller verfügbaren ODT-Template-Variablen
  • Link von Setup zur Variablen-Referenz

Modul-Descriptor

  • module_parts['models'] = 1
  • Neue Konstanten: MAHNUNG_ADDON_PDF, MAHNUNG_ADDON_PDF_ODT_PATH
  • Document-Model-Registrierung in init() (standard_mahnung + generic_mahnung_odt)
  • Widget box_mahnung_offen@mahnung registriert
  • Picto korrigiert: fa-envelope-open-text (FA5-Free)

Bugfixes

  • numero 500037 → 500038 (Kollision mit Eplan behoben)
  • verifCsrf() entfernt (existiert nicht in Dolibarr, CSRF via newToken())
  • f.statutf.fk_statut (Dolibarr 22.x Spaltenname)
  • actions_setmoduleoptions.inc.php vor llxHeader() verschoben (ODT-Upload)
  • Widget: require_once mahnung.class.php an Dateianfang (Fatal Error bei Klassen-Konstanten in SQL)

Entfernt

  • class/mahnungpdf.class.php — Logik in core/modules/mahnung/doc/pdf_standard_mahnung.modules.php

[0.1.0] — 2026-05-07 — Erstveröffentlichung (Phase 110)

DB-Schema (Phase 1)

  • llx_mahnung_mahnung — Mahnvorgänge mit Stufe, Beträgen, Zinsen, Status, Snapshot des Basiszinses für Reproduzierbarkeit
  • llx_mahnung_stufe — pro Stufe konfigurierbar: Frist, neue Frist, Gebühren B2C/B2B, optional Zinssatz-Override, Versandart, E-Mail-/PDF-Templates
  • 3 Default-Stufen werden bei Aktivierung idempotent eingefügt

Modul-Descriptor (Phase 1)

  • numero 500038, family financial, FA-Picto fa-envelope-open-text
  • Modul-Konstanten: MAHNUNG_BASISZINS, MAHNUNG_AUFSCHLAG_B2C, MAHNUNG_AUFSCHLAG_B2B, MAHNUNG_PAUSCHALE_B2B, MAHNUNG_NTFY_TOPIC
  • Rechte: read, write, send, delete, setup
  • Cron-Job MahnungCronBuildVorschlag (täglich, default deaktiviert)
  • Linkes Menü unter „Rechnungen" (mainmenu=billing) mit Vorschlagsliste / Archiv

CRUD + Setup (Phase 2)

  • class/mahnung.class.php — CRUD, Status-Konstanten, Verzugszinsen-Berechnung nach BGB §288
  • class/mahnungstufe.class.php — Stufen-Konfiguration, Override-Helfer für Zinsen/Gebühren
  • admin/setup.php — Stufen-Tabelle vollständig pflegbar, Konstanten persistent

Vorschlagsliste + Cron (Phase 3)

  • class/mahnungvorschlag.class.php — gemeinsamer Service: ermittelt pro überfälliger Rechnung die nächste vorgeschlagene Stufe, B2C/B2B-Erkennung via tva_intra, offener Betrag aus paiement_facture
  • class/mahnungcron.class.php — Cron sammelt Vorschläge, sendet Ntfy-Push (Topic aus Setup), schreibt zusätzlich GlobalNotify-Action wenn aktiv
  • class/mahnungntfy.class.php — schmaler Ntfy-Push-Wrapper
  • list.php — Vorschlagsliste-UI mit Multi-Select, Filter nach Stufe / Verzugstagen / Kunde, Buttons „Mahnungen erzeugen" und „Sammelbrief"

PDF-Generator + Erstellen (Phase 4)

  • TCPDF-basierter Generator (DIN-5008 Form A): Adressfenster, Bezugszeichenzeile, Tabelle, Gebührenblock, Verzugszinsen mit Snapshot-Zinssatz, neue Frist, Bankverbindungs-Footer
  • PDFs landen in documents/facture/{ref}/ und erscheinen automatisch im Dokumente-Tab der Rechnung
  • ajax/createmahnung.php — Bulk-Endpoint mit CSRF + Permission-Check, erzeugt Mahnung + PDF, behandelt §288 Abs. 5 Pauschale einmalig pro Rechnung

Hooks + Trigger (Phase 5)

  • core/triggers/interface_99_modMahnung_MahnungTriggers.class.phpBILL_PAYED und PAYMENT_CUSTOMER_CREATE setzen offene Mahnungen auf erledigt
  • class/actions_mahnung.class.php — Hook auf Rechnungs- und Kundenkarte: Tab „Mahnungen (n)" mit Badge, Button „Mahnung erstellen" wenn überfällig
  • card.php — Detailansicht eines Mahnvorgangs mit Storno-Aktion (formconfirm-Modal)

E-Mail + Sammelbrief (Phase 6)

  • ajax/sendmail.php — sendet Mahnung-PDF via CMailFile an die Kunden-Mail; Subject/Body mit Platzhaltern aus Stufen-Konfig
  • ajax/sammelbrief.php — erzeugt Mahnungen für Auswahl, konkateniert ihre PDFs via TCPDI in eine Datei, liefert Download

Integrationen (Phase 7 + 8)

  • GlobalNotify: Cron sendet zusätzlich actionRequired-Notification ins Dolibarr-UI (wenn Modul aktiv)
  • Tab „Mahnungen" auf Kundenkarte (thirdpartycard) zusätzlich zur Rechnungskarte

Pipeline (Phase 10)

  • .forgejo/workflows/deploy.yml — Deploy auf /mnt/appdata/firma/dolibarr-202509/modules/mahnung bei Push auf main mit [deploy] oder Tag v*, Ntfy-Notify auf Topic vk-builds