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>
240 lines
31 KiB
Markdown
240 lines
31 KiB
Markdown
# 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 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 0–127, 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 **Ziel**stufe, 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.statut` → `f.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 1–10)
|
||
|
||
### 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.php` — `BILL_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`
|