Widget-Prognose: Paritaet zur Kundenkarten-Box + Frueh-Signal statt "ueberfaellig" [deploy]
All checks were successful
Deploy mahnung / deploy (push) Successful in 13s

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>
This commit is contained in:
Eduard Wisch 2026-07-19 11:39:25 +02:00
parent adfeafa09b
commit 4997a78599
5 changed files with 20 additions and 13 deletions

View file

@ -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 = Ø(ZahlungRechnungsdatum) Ø(FälligkeitRechnungsdatum)` = Ø 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.

View file

@ -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 = Ø(ZahlungRechnungsdatum) Ø(FälligkeitRechnungsdatum)`; 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 = Ø(ZahlungRechnungsdatum) Ø(FälligkeitRechnungsdatum)`; 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)

View file

@ -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 &uuml;) — 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 = ' <span style="color:#dc3545;">('.dol_escape_htmltag($langs->transnoentities('MahnungProgUeberfaellig')).')</span>';
$hint = ' <span style="color:#dc3545;">('.dol_escape_htmltag($langs->transnoentities('MahnungProgSpaeterAlsUeblich')).')</span>';
}
// Parameter direkt an trans übergeben (macht das %s-sprintf selbst).
$tooltip = $langs->transnoentities('MahnungProgTooltip', $progDate, $avg, (string) $prog['count']);
$html = '<span title="'.dol_escape_htmltag($tooltip).'">';
$html .= '<span style="color:'.$rating['color'].';">'.$rating['icon'].'</span> ~'.$progDate.$ueberf;
$html .= '<span style="color:'.$rating['color'].';">'.$rating['icon'].'</span> ~'.$progDate.$hint;
$html .= '<br><span class="opacitymedium small">'.dol_escape_htmltag($label)
.' &middot; &#216; '.$avg.'&nbsp;T &middot; n='.((int) $prog['count']).'</span>';
$html .= '</span>';

View file

@ -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)
#

View file

@ -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)