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)