diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82ffc4b..af62718 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,8 +6,9 @@
- **Neue Spalte „Vsl. Zahlung" (Zahlungsprognose)**: Je offener Rechnung wird aus dem bisherigen Zahlungsverhalten des Kunden prognostiziert, wann er die Rechnung voraussichtlich zahlt (`Fälligkeit + round(Ø-Verzug)`), inkl. Ampel-Icon + Bewertungsstufe + Ø-Verzug + Stichprobengröße `n`.
- **Skala 1:1 aus dem BuchhaltungsWidget** übernommen (KB #886), damit die Ampel zur „Zahlungsverhalten"-Box auf der Kundenkarte passt: `diff = Ø(Zahlung−Rechnungsdatum) − Ø(Fälligkeit−Rechnungsdatum)` = Ø Tage nach Fälligkeit; Stufen ≤−5 Vorbildlich ★ / ≤0 Pünktlich ✓ / ≤7 Spätzahler ⚠ / ≤14 Verspätet ⏱ / >14 Problematisch ✗ (gleiche Farben `#28a745/#ffc107/#fd7e14/#dc3545`).
- **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`.
- - Prognosedatum bereits überschritten, Rechnung aber noch offen → Zusatz „(überfällig)" (Kunde zahlt schlechter als sonst = Mahnsignal).
- - Mindest-Stichprobe fürs Datum via Konstante `MAHNUNG_PROGNOSE_MIN_N` (Default 3), darunter „keine Historie". Neue Lang-Keys `MahnungProg*` (de_DE + en_US).
+ - Prognosedatum bereits verstrichen, Rechnung aber noch offen → Zusatz „(später als üblich)" — der Kunde zahlt langsamer als sein eigenes Muster (Frühwarnung; greift ggf. schon **vor** der formellen Fälligkeit, ist also bewusst nicht mit „überfällig" beschriftet).
+ - Prognosedatum + Ø-Verzug aus dem **exakten** Mittelwert (eine Nachkommastelle), damit die Zahl 1:1 zur „Differenz" auf der Kundenkarte passt (z.B. −3.1 T).
+ - Mindest-Stichprobe fürs Datum 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.
diff --git a/CLAUDE.md b/CLAUDE.md
index aa1edbb..dbc7d4a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -31,7 +31,7 @@ Dolibarr Custom-Modul: 3-stufiges Mahnwesen nach BGB §288 + Versand-Tracking +
- **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 (für den korrekten Zähler), rendert aber nur `MAHNUNG_BOX_MAXLINES` Zeilen + eine `...`-Überlaufzeile. **Nicht** auf das von Dolibarr übergebene `$max` verlassen — das kommt aus `MAIN_SIZE_SHORTLIST_LIMIT` (Default 5) und gilt global für ALLE Home-Boxen. (KB #598)
- **Empty-State Pflicht**: bei `$num == 0` Platzhalter-Zeile in `info_box_contents` einfügen — sonst rendert `ModeleBoxes::showBox()` gar nichts und das Widget verschwindet komplett (auch nach neuen Rechnungen sieht der User es nicht zurückkommen). Siehe KB #682.
- **Summenzeile Netto+Brutto**: Betragszelle der `liste_total`-Zeile zeigt zweizeilig Netto (`SUM(f.total_ht)`) und Brutto (`SUM(f.total_ttc)`). Brutto kommt direkt aus `f.total_ttc` der Rechnung, NICHT aus Netto × Steuersatz hochgerechnet — sonst wären Reverse-Charge §13b, Steuerbefreiung und Kleinunternehmer §19 UStG falsch. Lang-Keys `MahnungBoxNetto`/`MahnungBoxBrutto`.
-- **Spalte „Vsl. Zahlung" (Zahlungsprognose)**: `getZahlprognose()`/`buildPrognoseCell()`/`prognoseRating()`. Skala + Berechnung sind eine **self-contained Kopie aus BuchhaltungsWidget** (`getPaymentStatistics()`), Referenz **KB #886** — bei Skala-Änderungen BEIDE Module synchron halten (Schwellen ≤−5/≤0/≤7/≤14, Farben `#28a745/#ffc107/#fd7e14/#dc3545`, Filter `type IN (0,1,5)` + `fk_statut=2`+`paye=1`+`date_lim_reglement IS NOT NULL`). `diff = Ø(Zahlung−Rechnungsdatum) − Ø(Fälligkeit−Rechnungsdatum)`; Prognosedatum = `Fälligkeit + round(diff)`; Mindest-`n` via `MAHNUNG_PROGNOSE_MIN_N` (Default 3), darunter „keine Historie". Die **Kundenkarten-Statistik selbst liefert BuchhaltungsWidget** (Hook `tabContentViewThirdparty`) — Mahnung baut dort KEINEN zweiten Block. **`$langs->transnoentities(...)` verwenden** (nicht `trans()`+`sprintf` → leere `%s`; nicht `trans()`+`dol_escape_htmltag` → doppeltes `&`-Encoding).
+- **Spalte „Vsl. Zahlung" (Zahlungsprognose)**: `getZahlprognose()`/`buildPrognoseCell()`/`prognoseRating()`. Skala + Berechnung sind eine **self-contained Kopie aus BuchhaltungsWidget** (`getPaymentStatistics()`), Referenz **KB #886** — bei Skala-Änderungen BEIDE Module synchron halten (Schwellen ≤−5/≤0/≤7/≤14, Farben `#28a745/#ffc107/#fd7e14/#dc3545`, Filter `type IN (0,1,5)` + `fk_statut=2`+`paye=1`+`date_lim_reglement IS NOT NULL`). `diff = Ø(Zahlung−Rechnungsdatum) − Ø(Fälligkeit−Rechnungsdatum)`; Prognosedatum = `Fälligkeit + diff` (exakt, aus dem Timestamp); Mindest-`n` via `MAHNUNG_PROGNOSE_MIN_N` (**Default 1** = Parität zur Kundenkarten-Box). Prognose verstrichen + Rechnung offen → „(später als üblich)" (NICHT „überfällig" — greift ggf. schon vor formeller Fälligkeit). Die **Kundenkarten-Statistik selbst liefert BuchhaltungsWidget** (Hook `tabContentViewThirdparty`) — Mahnung baut dort KEINEN zweiten Block. **`$langs->transnoentities(...)` verwenden** (nicht `trans()`+`sprintf` → leere `%s`; nicht `trans()`+`dol_escape_htmltag` → doppeltes `&`-Encoding).
## Hooks-Stolperfallen
- **`completeTabsHead`** wird bei jedem Aufruf von `complete_head_from_modules()` getriggert — pro Karte mehrfach (core + external + remove). Filter auf `mode=add` + `filterorigmodule=external`, sonst doppelter Tab. (KB #601)
diff --git a/core/boxes/box_mahnung_offen.php b/core/boxes/box_mahnung_offen.php
index a02422e..45a9081 100644
--- a/core/boxes/box_mahnung_offen.php
+++ b/core/boxes/box_mahnung_offen.php
@@ -406,7 +406,10 @@ class box_mahnung_offen extends ModeleBoxes
global $langs;
$prog = $this->getZahlprognose($socid);
- $minN = getDolGlobalInt('MAHNUNG_PROGNOSE_MIN_N', 3);
+ // Default 1 = Parität zur "Zahlungsverhalten"-Box (BuchhaltungsWidget, KB #886),
+ // die ab der 1. bezahlten Rechnung bewertet. Höher setzen, wenn Einzelrechnungs-
+ // Prognosen als zu unsicher empfunden werden.
+ $minN = getDolGlobalInt('MAHNUNG_PROGNOSE_MIN_N', 1);
// transnoentities() liefert rohes UTF-8 (kein ü) — sonst würde das
// nachfolgende dol_escape_htmltag() das & doppelt kodieren.
@@ -419,22 +422,25 @@ class box_mahnung_offen extends ModeleBoxes
$rating = $this->prognoseRating($prog['diff']);
$label = $langs->transnoentities($rating['labelkey']);
- $tage = (int) round($prog['diff']);
- $progTs = (int) $datelimit + ($tage * 86400);
+ // Prognosedatum aus dem EXAKTEN Ø-Verzug (gleiche Zahl wie "Differenz" auf der
+ // Kundenkarte); Anzeige mit einer Nachkommastelle, damit z.B. −3.1 exakt passt.
+ $progTs = (int) round((int) $datelimit + $prog['diff'] * 86400);
$progDate = dol_print_date($progTs, 'day', 'tzuserrel');
- $avg = sprintf('%+d', $tage); // Vorzeichen: + nach Fälligkeit, − davor
+ $avg = sprintf('%+.1f', $prog['diff']); // Vorzeichen: + nach Fälligkeit, − davor
- // Prognosedatum überschritten, Rechnung noch offen → schlechter als sonst
- $ueberf = '';
+ // Prognosedatum bereits verstrichen, Rechnung aber noch offen → der Kunde zahlt
+ // langsamer als sein eigenes Muster. NICHT dasselbe wie "überfällig" (kann schon
+ // vor der formellen Fälligkeit greifen) — daher eigene Formulierung.
+ $hint = '';
if ($progTs < dol_now()) {
- $ueberf = ' ('.dol_escape_htmltag($langs->transnoentities('MahnungProgUeberfaellig')).')';
+ $hint = ' ('.dol_escape_htmltag($langs->transnoentities('MahnungProgSpaeterAlsUeblich')).')';
}
// Parameter direkt an trans übergeben (macht das %s-sprintf selbst).
$tooltip = $langs->transnoentities('MahnungProgTooltip', $progDate, $avg, (string) $prog['count']);
$html = '';
- $html .= ''.$rating['icon'].' ~'.$progDate.$ueberf;
+ $html .= ''.$rating['icon'].' ~'.$progDate.$hint;
$html .= '
'.dol_escape_htmltag($label)
.' · Ø '.$avg.' T · n='.((int) $prog['count']).'';
$html .= '';
diff --git a/langs/de_DE/mahnung.lang b/langs/de_DE/mahnung.lang
index e96bb44..9d0d488 100644
--- a/langs/de_DE/mahnung.lang
+++ b/langs/de_DE/mahnung.lang
@@ -220,7 +220,7 @@ MahnungProgVerspaetet = Verspätet
MahnungProgProblematisch = Problematisch
MahnungProgKeineHistorie = keine Historie
MahnungProgKeineHistorieTip = Zu wenig bezahlte Rechnungen für eine belastbare Zahlungsprognose
-MahnungProgUeberfaellig = überfällig
+MahnungProgSpaeterAlsUeblich = später als üblich
MahnungProgTooltip = Voraussichtlich am %s (im Schnitt %s Tage zur Fälligkeit, %s bezahlte Rechnungen)
#
diff --git a/langs/en_US/mahnung.lang b/langs/en_US/mahnung.lang
index 7b28a4b..62079c9 100644
--- a/langs/en_US/mahnung.lang
+++ b/langs/en_US/mahnung.lang
@@ -345,7 +345,7 @@ MahnungProgVerspaetet = Late
MahnungProgProblematisch = Problematic
MahnungProgKeineHistorie = no history
MahnungProgKeineHistorieTip = Too few paid invoices for a reliable payment forecast
-MahnungProgUeberfaellig = overdue
+MahnungProgSpaeterAlsUeblich = later than usual
MahnungProgTooltip = Expected on %s (on average %s days relative to due date, %s paid invoices)
MahnungVorschlagStufeNichtKonfiguriert = Stage 1 not configured
MahnungVorschlagFristNichtErreicht = Stage 1 deadline (%s days) not yet reached (overdue %s days)