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>
Eddy-Feedback: Die Differenz-zur-Faelligkeit (z.B. -13.5 T) war unintuitiv
(Kopfrechnen noetig) und "(spaeter als ueblich)" hat die Widget-Spalte
gesprengt.
- Prognosedatum jetzt am RECHNUNGSDATUM verankert: datef + Oe Tage nach
Rechnungseingang (avg_pay). Das ist die klassische Days-to-Pay-Kennzahl,
vermeidet Prognosen vor dem Rechnungsdatum und ist direkt lesbar.
- Sichtbare Zahl = "Oe X T nach Rechnung" (positiv) statt Differenz zur
Faelligkeit. Ampel-Icon bleibt auf der Faelligkeits-Skala (diff) -> weiter
Paritaet zur Zahlungsverhalten-Box der Kundenkarte.
- "(spaeter als ueblich)" als Text entfernt: bei verstrichener Prognose
wird stattdessen nur das DATUM rot gefaerbt (Zeile bleibt schmal),
Erklaerung steckt im Tooltip.
- getZahlprognose liefert zusaetzlich avg_pay; buildPrognoseCell bekommt
das Rechnungsdatum durchgereicht. Neuer Lang-Key MahnungProgNachRechnung,
Tooltip umformuliert.
Lokal gegen die Test-DB verifiziert (Datum aus datef+avg_pay, Rot nur bei
verstrichener Prognose, Zukunfts-Rechnung ohne Rot, php -l sauber).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix nach Eddys Beobachtung: Kunde mit Historie auf der Kundenkarte
(BuchhaltungsWidget bewertet ab n>=1) erschien im Widget als "keine
Historie", weil MAHNUNG_PROGNOSE_MIN_N auf 3 stand.
- MIN_N Default 3 -> 1: Paritaet zur Zahlungsverhalten-Box, die ebenfalls
ab der 1. bezahlten Rechnung bewertet. Weiter per Konstante erhoehbar.
- "(ueberfaellig)" -> "(spaeter als ueblich)": Das Flag greift wenn das
Prognosedatum verstrichen und die Rechnung noch offen ist - das kann
schon VOR der formellen Faelligkeit passieren (Fruehzahler). "ueberfaellig"
war dafuer das falsche Wort. Neuer Lang-Key MahnungProgSpaeterAlsUeblich.
- Prognosedatum + OE-Verzug jetzt aus dem EXAKTEN Mittelwert (Timestamp
statt gerundeter Tage), Anzeige mit einer Nachkommastelle -> deckt sich
1:1 mit "Differenz" auf der Kundenkarte (z.B. -3.1 T -> Faelligkeit-3.1).
Lokal gegen die Test-DB verifiziert (n=1/n=2 werden gezeigt, Flag nur bei
verstrichener Prognose, Zukunfts-Faelligkeit ohne Flag, php -l sauber).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Neue Spalte "Vsl. Zahlung" in box_mahnung_offen: je offener Rechnung
Prognose (Faelligkeit + round(Oe-Verzug)) aus dem bisherigen Zahlungs-
verhalten des Kunden, mit Ampel-Icon + Bewertungsstufe + Tooltip.
- Skala 1:1 aus BuchhaltungsWidget uebernommen (KB #886), damit die
Ampel zur "Zahlungsverhalten"-Box auf der Kundenkarte passt: 5 Stufen
Vorbildlich/Puenktlich/Spaetzahler/Verspaetet/Problematisch anhand
Oe-Tage nach Faelligkeit, gleiche Schwellen + Farben.
- Berechnung als self-contained Kopie (getZahlprognose, 1 Query je
Kunde/Request gecacht) - keine Laufzeit-Abhaengigkeit auf das
BuchhaltungsWidget.
- Prognose ueberschritten + Rechnung noch offen -> "(ueberfaellig)"
als zusaetzliches Mahnsignal.
- Mindest-Stichprobe MAHNUNG_PROGNOSE_MIN_N (Default 3), sonst
"keine Historie". Neue Lang-Keys MahnungProg* (de_DE + en_US).
- $langs->transnoentities() statt trans() genutzt (sonst leere %s
durch doppeltes sprintf bzw. doppeltes &-Encoding, siehe KB #897).
End-to-end via CLI-Harness gegen die lokale Test-DB verifiziert
(alle 5 Stufen, Overdue-Zweig, Empty-State, php -l sauber).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tms war als reines TIMESTAMP angelegt -> unter explicit_defaults_for_timestamp
NULL DEFAULT NULL, blieb daher bei jedem UPDATE leer. Jetzt Dolibarr-Standard
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP fuer mahnung/stufe/
trackingpattern. Neue idempotente Migration migrateTimestampSpalten() im init()
befuellt Alt-NULLs aus datec und stellt die Spalte per ALTER TABLE um.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bonitaets-Anzeige:
- Hook tabContentViewThirdparty rendert prominente rote Warnbox auf der
Kundenkarte wenn fk_statut=3 + close_code=badcustomer existiert. Zeigt
Anzahl, Gesamtsumme, Datum letzter Abschreibung + Link zur Detail-Liste.
- Hook formObjectOptions zeigt kompakte Warn-Zeile auf ordercard und
invoicecard wenn der Kunde Forderungsausfaelle hat.
- ordercard zum module_parts.hooks.data ergaenzt.
Uneinbringlich-Button:
- Auf Mahnung-Karten der Stufe 3, Status >= ERSTELLT, nicht storniert,
Rechnung noch nicht abandoned.
- Bestaetigungs-Dialog mit Begruendungs-Textfeld (Default-Text setzt
das aktuelle Datum ein).
- Ruft Facture::setCanceled mit CommonInvoice::CLOSECODE_BADDEBT.
- Mahnung wird storniert + Begruendung in note_private festgehalten.
Steuer-Modul kompatibel: EÜR liest nur llx_paiement (keine Zahlung =
keine Einnahme), UStVA filtert fk_statut IN (1,2) — abandoned Rechnungen
werden automatisch korrekt ausgeschlossen. Bei Ist-Versteuerung damit
buchhalterisch sauber, kein manueller Eingriff noetig.
Lang-Keys: 16 neu (de_DE + en_US) fuer Bonitaets-Box + Uneinbringlich-Workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug:
- Mein letzter Commit hat einen plain-SQL-Filter an select_company uebergeben.
Dolibarrs select_thirdparty_list reicht den Filter durch
forgeSQLFromUniversalSearchCriteria, das erwartet aber USC-Syntax
feld:operator:wert in Klammern. Plain-SQL fuehrt zu SQL-Syntax-Error
und 500-Antwort.
Fix:
- B2B-Filter: (s.tva_intra:isnot:NULL) AND (s.tva_intra:!=:'')
- B2C-Filter: (s.tva_intra:is:NULL) OR (s.tva_intra:=:'')
Zusatz-Fixes aus dem Log:
- search_socid=-1 (von select_company als "nichts ausgewaehlt" gerendert)
wurde irrtuemlich als Filter auf fk_soc=-1 angewendet. Jetzt nur als
Filter genutzt wenn > 0.
- Beim Auto-Submit des Kundentyp-Selects wird search_socid auf "" gesetzt,
damit eine zuvor ausgewaehlte (jetzt evtl. ausgefilterte) Kunden-ID
nicht stehen bleibt.
KB-Eintrag #602 zur USC-Syntax angelegt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Kundentyp filtert das Kunden-Dropdown:
- select_company bekommt SQL-Filter "s.tva_intra IS NOT NULL AND s.tva_intra <> ''"
bzw. "s.tva_intra IS NULL OR s.tva_intra = ''" je nach B2B/B2C-Wahl.
- Das Kundentyp-Select hat onchange=this.form.submit, sodass das Dropdown
ohne extra Klick auf "Suche" direkt neu geladen wird.
Skip-Grund-Zelle:
- opacitymedium-Klasse von td auf inneres span verschoben. Manche Themes
rendern td.opacitymedium mit eigenem Border-Verhalten — das hatte zu
sichtbarem Rahmen-Unterschied in der Uebersprungen-Tabelle gefuehrt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Cron-Job MahnungCronVersandReminder (taeglich):
- Sucht in llx_mahnung_mahnung Status=ERSTELLT (1) + date_versand IS NULL
+ datec < NOW() - INTERVAL N DAY.
- N steht in der Konstante MAHNUNG_VERSAND_REMINDER_DAYS (Default 2).
- Bei Treffern: Ntfy-Push (Topic MAHNUNG_NTFY_TOPIC) mit Titel + Liste
der bis zu 8 Mahnungen ("MAHN2026-0042 (Stufe 2, 3 Tage alt) — Kunde").
- Optional GlobalNotify-Badge "mahnung_versand" wenn GlobalNotify aktiv.
Modul-Descriptor:
- Cronjobs-Array um Reminder ergaenzt (frequency 1d, priority 55, status 1).
Lang-Keys: 2x (de_DE + en_US) fuer Cron-Label + Description.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Button "Belege scannen" im Versand-Block der Mahnungs-Karte:
- Action scan_belege durchlaeuft alle Files in DOL_DATA_ROOT/mahnung/<MAHN-Ref>/
- PDFs werden via pdftotext (CLI, mit Verfuegbarkeits-Check) extrahiert
- txt/html werden direkt eingelesen (HTML mit strip_tags)
- Pro Datei wird MahnungTrackingPattern::detectFromText() angewendet —
matched gegen alle aktiven Patterns nach priority DESC
- Treffer landen in $_SESSION als Vorschlag (file, provider, nr, url, label)
UX:
- Vorschlags-Banner mit gruener Linke ueber dem Beleg-Bereich
- Pro Vorschlag: Datei-Icon, Pattern-Label, Sendungsnummer als <code>,
externer Link zur Sendungsverfolgung, "Uebernehmen"-Button
- "Uebernehmen" (action=apply_tracking) speichert tracking_nr +
tracking_provider an der Mahnung und leert Session
- "Verwerfen" (action=dismiss_tracking) entfernt nur Session-Eintrag
Fallback:
- Wenn pdftotext nicht im Container verfuegbar: Warnmeldung im UI,
txt/html werden trotzdem verarbeitet.
OCR fuer Bilder (PNG/JPG) bewusst noch nicht enthalten — separater Schritt
mit Container-Anpassung (Tesseract) wenn gewuenscht.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filter-Zeile:
- Kunden-Filter "rowid"-Input ersetzt durch $form->select_company()
(Ajax-Suche falls COMPANY_USE_SEARCH_TO_SELECT gesetzt, sonst klassisches
Dropdown). Direkt-Links ?search_socid=74 von der Kundenkarte bleiben
weiterhin funktional (htmlname=search_socid, Backward-kompatibel).
- Neuer Filter "Mindestbetrag" (EUR, Komma zugelassen).
- Neuer Filter "Kundentyp" (alle / B2B / B2C).
Tabelle:
- Neue Spalte "Kontakt" mit Telefon- und Mail-Direktlink-Icons (tel: / mailto:).
- Spalte erscheint sowohl in der Vorschlags- als auch in der Uebersprungen-Tabelle.
MahnungVorschlag::getVorschlaege() + buildAlleVorschlaege():
- SELECT erweitert um s.phone + s.email; werden als soc_phone/soc_email
pro Eintrag mitgegeben.
- Neue PHP-side Filter min_betrag und kundentyp.
Lang-Keys: MahnungKontakt (de_DE + en_US).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dolibarr ruft complete_head_from_modules() pro societe_prepare_head() mehrmals
auf (core/lib/company.lib.php:365 fuer 'core', 487 fuer 'external', 489 fuer
'remove'). Jeder Aufruf feuert completeTabsHead — der Hook haengte den Tab
entsprechend mehrfach an.
Fix: im Hook auf $parameters['mode']='add' UND filterorigmodule='external'
filtern, damit der Tab genau einmal pro Karte erscheint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>