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>
31 KiB
31 KiB
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_erinnerung— nicht die Stufennummer. Eine Erinnerung kostet nichts: Mahngebühr, §288-Abs.-5-Pauschale und Verzugszinsen werden inmahnungBaueVorgang()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 0–127, Lücken erlaubt). Einstieg ist die kleinste aktive Stufe (
minStufe()), die Folgestufe liefertnaechsteStufeNach()— Lücken werden übersprungen. Auch der Button „Als uneinbringlich klassifizieren" hängt nicht mehr anstufe === 3, sondern daran, dass keine weitere aktive Stufe mehr folgt. - Wartefrist korrigiert: maßgeblich ist
frist_tageder Zielstufe, nichtneue_frist_tageder Vorstufe. Vorher warfrist_tageder Stufen 2/3 ein toter Konfigurationswert. Bezugspunkt istdate_versand, Fallbackdate_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, weilrechneSumme()nur die aktuelle Stufe addierte. - Zinssatz-Spalten von
DECIMAL(5,4)aufDECIMAL(6,3):5,4konnte 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 vonlist.php,card.phpund den AJAX-Endpoints). Ohne das schlug nach jedem Deploy jedescreate()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 dierowidab und prüft die Firmenzugehörigkeit nicht — ein manipuliertesreceiver[]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 überFacture::generateDocument()nacherzeugt. Weitere Dateien lassen sich anhängen und wieder entfernen. Bewusst nicht Dolibarrsmode=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.
- Empfängerauswahl enthält die Firmenadresse und alle aktiven Ansprechpartner des Kunden (
ajax/sendmail.phpist 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
UPDATEatomar 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_mahnungnachgezogen, damit Mail und Datensatz denselben Betrag nennen. Die neue Zahlungsfrist wird ausneue_frist_tageneu 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_mailprotokollstatt 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_VERSIONsteht 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()inlib/mahnung_ui.lib.php, gesetzt über denmorehtmlright-Parameter vonload_fiche_titre()). Sichtbar nur mit dem Rechtmahnung.setupoder 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 dortDolEditormit derselben Toolbar und derselben Konstante (FCKEDITOR_ENABLE_MAIL) wie bei Dolibarrs eigenen Mailvorlagen. Das Mailformular auf der Karte nutztwithfckeditor = -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.forcewird 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 durchdol_nl2br()— allein durch das Abschicken des Formulars wurde aus dem Mailtext HTML mit<br>-Tags. Sichtbar wurde das doppelt: die Tags standen im Textfeld, unddol_textishtml()stufte den Text danach als HTML ein, sodass eine als Klartext gepflegte Erinnerung als HTML-Mail rausgegangen wäre. Neue FunktionmahnungBodyEntkleiden()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
mailinitplus 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 mitallowoverwrite, 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:
presendist token-pflichtig (die Action stellt die Rechnungs-PDF bereit und erzeugt sie notfalls neu), ebenso die Core-Dateiaktionen auscore/actions_linkedfiles.inc.php(confirm_deletefile,confirm_updateline,renamefilesowie die parameterbasiertensendit/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.phpmitmahnungStufeFarbe()undmahnungStufeBadge(). Die Farbskala war vorher inlist.phpundcore/boxes/box_mahnung_offen.phpdoppelt 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 aufFälligkeit + diff, fallsdateffehlt. - 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-KeysMahnungProg*(de_DE + en_US).
- Prognose am Rechnungsdatum verankert:
- 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 ausf.total_ttcder 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-KeysMahnungBoxNetto/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 insetup.php: Alle/5/10/20/30/50, Default0= alle). Widget lädt immer alle offenen Rechnungen (korrekter Zähler), rendert aber nurMAHNUNG_BOX_MAXLINESZeilen + eine...-Überlaufzeile.
Schema-Fix (tms-Spalten)
- Die
tms-Spalten vonllx_mahnung_mahnung,llx_mahnung_stufeundllx_mahnung_trackingpatternwurden als reinesTIMESTAMPangelegt — unterexplicit_defaults_for_timestampentstand darausNULL DEFAULT NULL, sodasstmsbei jedem UPDATE leer blieb. Jetzt Dolibarr-StandardDEFAULT 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 bestehendeNULL-Werte ausdatecund stellt die Spalte perALTER TABLEum. Greift auf Bestands-Installs beim Re-Aktivieren des Moduls.
Bonitaet / Forderungsausfall-Workflow
- Neuer Hook
tabContentViewThirdpartyrendert 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
formObjectOptionszeigt eine kompakte Warn-Zeile bei Auftrags-/Rechnungs-Karten ("ordercard", "invoicecard"), wenn der Kunde Forderungsausfaelle hat — Bonitaets-Pruefung vor neuem Geschaeft. - Neuer Hook-Kontext
ordercardzum 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 auffk_statut=3+close_code='badcustomer' - Mahnung wird zugleich storniert und die Begruendung in
note_privatefestgehalten
- Steuerlich passt das: Dolibarrs Steuer-Modul (EÜR) ignoriert abandoned Rechnungen automatisch (UStVA filtert
fk_statut IN (1,2); EÜR liest nurllx_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 durchforgeSQLFromUniversalSearchCriteriafehlschlagen. - 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(vonselect_companyals "nichts ausgewaehlt" geliefert) wird jetzt korrekt ignoriert statt als Filter auffk_soc=-1zu wirken.- Skip-Grund-Spalte in der Uebersprungen-Tabelle:
opacitymediumjetzt am innerenspanstatt amtd, damit Theme-spezifisches Zellen-Border-Verhalten konsistent bleibt.
Versand-Reminder (Cron + Ntfy)
- Neuer Cron-Job
MahnungCronVersandReminder(taeglich): sucht Mahnungen mit StatusERSTELLTderen 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
pdftotextnur Form-Feed-Zeichen liefert (Bild-PDF), wird automatischocrmypdf --skip-text -l deu+engaufgerufen und das OCR-PDF erneut mitpdftotextgelesen. 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_providergespeichert; zusaetzlich werdendate_versandundversandwegautomatisch 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.phpmit CRUD: Pattern anlegen/bearbeiten/aktivieren-deaktivieren/loeschen. - Live-Vorschau: Beim Tippen von Regex/URL/Beispieltext wird via AJAX-Endpoint
ajax/regex_preview.phpdirekt 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 wirdMahnungTrackingPattern::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_provideranllx_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 inDOL_DATA_ROOT/mahnung/<MAHN-Ref>/, voll integriert mit ECM/document.php. - Status springt automatisch auf
STATUS_VERSENDETsobald 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=74bleiben 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_snapshotwird 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_dirwar imscan_belege-Handler undefiniert (Variable wurde erst 90 Zeilen spaeter gesetzt) — eigene Pfadberechnung im Action-Block.pdftotextForm-Feed-Zeichen (\x0C) bei Bild-PDFs:trim()entfernt\x0Cnicht — explizite Zeichenliste noetig.- Kundenkarte: Tab "Mahnwesen" erschien doppelt, weil
complete_head_from_modules()pro Karte mehrfach (core + external + remove) feuert. Hook filtert jetzt aufmode=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_odtmit Stufen-spezifischer Template-Auswahl (mahnung_stufe1/2/3.odt, Fallback mahnung.odt) - TCPDF-Generator
pdf_standard_mahnung(refactored ausmahnungpdf.class.php) mahnung.class.php:generateDocument()Methode viacommonGenerateDocument()- 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@mahnungregistriert - Picto korrigiert:
fa-envelope-open-text(FA5-Free)
Bugfixes
- numero 500037 → 500038 (Kollision mit Eplan behoben)
verifCsrf()entfernt (existiert nicht in Dolibarr, CSRF vianewToken())f.statut→f.fk_statut(Dolibarr 22.x Spaltenname)actions_setmoduleoptions.inc.phpvorllxHeader()verschoben (ODT-Upload)- Widget:
require_once mahnung.class.phpan Dateianfang (Fatal Error bei Klassen-Konstanten in SQL)
Entfernt
class/mahnungpdf.class.php— Logik incore/modules/mahnung/doc/pdf_standard_mahnung.modules.php
[0.1.0] — 2026-05-07 — Erstveröffentlichung (Phase 1–10)
DB-Schema (Phase 1)
llx_mahnung_mahnung— Mahnvorgänge mit Stufe, Beträgen, Zinsen, Status, Snapshot des Basiszinses für Reproduzierbarkeitllx_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, familyfinancial, FA-Pictofa-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 §288class/mahnungstufe.class.php— Stufen-Konfiguration, Override-Helfer für Zinsen/Gebührenadmin/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 viatva_intra, offener Betrag auspaiement_factureclass/mahnungcron.class.php— Cron sammelt Vorschläge, sendet Ntfy-Push (Topic aus Setup), schreibt zusätzlich GlobalNotify-Action wenn aktivclass/mahnungntfy.class.php— schmaler Ntfy-Push-Wrapperlist.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.php—BILL_PAYEDundPAYMENT_CUSTOMER_CREATEsetzen offene Mahnungen auf erledigtclass/actions_mahnung.class.php— Hook auf Rechnungs- und Kundenkarte: Tab „Mahnungen (n)" mit Badge, Button „Mahnung erstellen" wenn überfälligcard.php— Detailansicht eines Mahnvorgangs mit Storno-Aktion (formconfirm-Modal)
E-Mail + Sammelbrief (Phase 6)
ajax/sendmail.php— sendet Mahnung-PDF viaCMailFilean die Kunden-Mail; Subject/Body mit Platzhaltern aus Stufen-Konfigajax/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/mahnungbei Push aufmainmit[deploy]oder Tagv*, Ntfy-Notify auf Topicvk-builds