diff --git a/CHANGELOG.md b/CHANGELOG.md index af62718..8d53b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,12 @@ ## [Unreleased] ### Widget (box_mahnung_offen) -- **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`). +- **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 `Ø nach Rechnung · n=`. + - **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`. - - 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). + - **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. diff --git a/CLAUDE.md b/CLAUDE.md index dbc7d4a..fa0586c 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 + 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). +- **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`). Prognosedatum = `datef + avg_pay` (am **Rechnungsdatum** verankert, avg_pay = Ø Tage nach Rechnungseingang), Fallback `Fälligkeit + diff`. Sichtbare Zahl = „Ø X T nach Rechnung" (intuitive Days-to-Pay, NICHT die Differenz zur Fälligkeit — die war zu unintuitiv, Eddy-Feedback). Ampel-Icon aber weiter über `diff = Ø Tage nach Fälligkeit` (Parität zur Kundenkarte). Mindest-`n` via `MAHNUNG_PROGNOSE_MIN_N` (**Default 1**). Prognose verstrichen + Rechnung offen → **Datum wird rot** (kein Zusatztext — sprengt sonst die Spalte; „später als üblich" nur im Tooltip). 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 45a9081..68ec38a 100644 --- a/core/boxes/box_mahnung_offen.php +++ b/core/boxes/box_mahnung_offen.php @@ -147,6 +147,7 @@ class box_mahnung_offen extends ModeleBoxes $objp = $this->db->fetch_object($result); $datelimit = $this->db->jdate($objp->datelimit); + $dateinvoice = $this->db->jdate($objp->date); $facturestatic->id = $objp->facid; $facturestatic->ref = $objp->ref; @@ -223,7 +224,7 @@ class box_mahnung_offen extends ModeleBoxes // Spalte 5: Voraussichtliche Zahlung (Prognose aus dem Zahlungsverhalten) $this->info_box_contents[$line][] = array( 'td' => 'class="center nowraponall"', - 'text' => $this->buildPrognoseCell((int) $objp->socid, $datelimit), + 'text' => $this->buildPrognoseCell((int) $objp->socid, $datelimit, $dateinvoice), 'asis' => 1, ); @@ -358,8 +359,11 @@ class box_mahnung_offen extends ModeleBoxes $o = $this->db->fetch_object($res); if ($o && (int) $o->invoice_count > 0) { $out = array( - 'count' => (int) $o->invoice_count, - 'diff' => (float) $o->avg_payment_days - (float) $o->avg_due_days, + 'count' => (int) $o->invoice_count, + // avg_pay = Ø Tage nach Rechnungseingang (intuitive "Days to Pay"-Zahl). + 'avg_pay' => (float) $o->avg_payment_days, + // diff = Ø Tage nach Fälligkeit (nur für die Ampel-Bewertung, KB #886). + 'diff' => (float) $o->avg_payment_days - (float) $o->avg_due_days, ); } $this->db->free($res); @@ -394,14 +398,20 @@ class box_mahnung_offen extends ModeleBoxes /** * Baut die HTML-Zelle "Vsl. Zahlung": Ampel-Icon + Prognosedatum + Kurzstatistik. - * Prognosedatum = Fälligkeit + round(diff). Prognose bereits vorbei, Rechnung aber - * noch offen → Zusatz "(überfällig)" (Kunde zahlt schlechter als sonst). + * + * Prognose = Rechnungsdatum (datef) + Ø Tage nach Rechnungseingang (avg_pay) — + * die intuitive "Days to Pay"-Kennzahl. Verstrichenes Prognosedatum bei noch + * offener Rechnung → Datum wird rot eingefärbt (kompakt, Erklärung im Tooltip), + * KEIN zusätzlicher Text (würde die Widget-Spalte zu breit machen). + * Ampel-Icon bleibt auf der Fälligkeits-Skala (diff), damit es zur + * "Zahlungsverhalten"-Box der Kundenkarte passt. * * @param int $socid - * @param int $datelimit Fälligkeit als Unix-Timestamp (jdate) - * @return string HTML (wird 'asis' gerendert) + * @param int $datelimit Fälligkeit als Unix-Timestamp (jdate) — Fallback-Anker + * @param int $dateinvoice Rechnungsdatum (datef) als Unix-Timestamp (jdate) + * @return string HTML (wird 'asis' gerendert) */ - private function buildPrognoseCell($socid, $datelimit) + private function buildPrognoseCell($socid, $datelimit, $dateinvoice) { global $langs; @@ -422,27 +432,34 @@ class box_mahnung_offen extends ModeleBoxes $rating = $this->prognoseRating($prog['diff']); $label = $langs->transnoentities($rating['labelkey']); - // 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); + // Prognosedatum = Rechnungsdatum + Ø Tage nach Rechnungseingang. Verankerung am + // Rechnungsdatum (statt Fälligkeit) ist intuitiv und vermeidet Prognosen VOR dem + // Rechnungsdatum. Fallback auf Fälligkeit + diff, falls datef fehlt. + if ((int) $dateinvoice > 0) { + $progTs = (int) round((int) $dateinvoice + $prog['avg_pay'] * 86400); + } else { + $progTs = (int) round((int) $datelimit + $prog['diff'] * 86400); + } $progDate = dol_print_date($progTs, 'day', 'tzuserrel'); - $avg = sprintf('%+.1f', $prog['diff']); // Vorzeichen: + nach Fälligkeit, − davor + $nachRe = sprintf('%.1f', $prog['avg_pay']); // Ø Tage nach Rechnungseingang (i.d.R. positiv) - // 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 = ''; + // Prognosedatum verstrichen, Rechnung noch offen → zahlt langsamer als sein Muster. + // Kompakt: nur das Datum rot färben + Tooltip-Zusatz (kein Extra-Text in der Zelle). + $dateStyle = ''; + $tipExtra = ''; if ($progTs < dol_now()) { - $hint = ' ('.dol_escape_htmltag($langs->transnoentities('MahnungProgSpaeterAlsUeblich')).')'; + $dateStyle = ' style="color:#dc3545;"'; + $tipExtra = ' — '.$langs->transnoentities('MahnungProgSpaeterAlsUeblich'); } - // Parameter direkt an trans übergeben (macht das %s-sprintf selbst). - $tooltip = $langs->transnoentities('MahnungProgTooltip', $progDate, $avg, (string) $prog['count']); + // Voller Kontext im Tooltip (Parameter direkt an trans → macht das %s-sprintf selbst). + $tooltip = $label.': '.$langs->transnoentities('MahnungProgTooltip', $progDate, $nachRe, (string) $prog['count']).$tipExtra; $html = ''; - $html .= ''.$rating['icon'].' ~'.$progDate.$hint; - $html .= '
'.dol_escape_htmltag($label) - .' · Ø '.$avg.' T · n='.((int) $prog['count']).''; + $html .= ''.$rating['icon'].' '; + $html .= '~'.$progDate.'
'; + $html .= '
Ø '.$nachRe.' ' + .dol_escape_htmltag($langs->transnoentities('MahnungProgNachRechnung')).' · n='.((int) $prog['count']).''; $html .= ''; return $html; diff --git a/langs/de_DE/mahnung.lang b/langs/de_DE/mahnung.lang index 9d0d488..3dfd365 100644 --- a/langs/de_DE/mahnung.lang +++ b/langs/de_DE/mahnung.lang @@ -221,7 +221,8 @@ MahnungProgProblematisch = Problematisch MahnungProgKeineHistorie = keine Historie MahnungProgKeineHistorieTip = Zu wenig bezahlte Rechnungen für eine belastbare Zahlungsprognose MahnungProgSpaeterAlsUeblich = später als üblich -MahnungProgTooltip = Voraussichtlich am %s (im Schnitt %s Tage zur Fälligkeit, %s bezahlte Rechnungen) +MahnungProgNachRechnung = T nach Rechnung +MahnungProgTooltip = voraussichtlich am %s — zahlt Ø %s Tage nach Rechnungseingang (%s bezahlte Rechnungen) # # Dokumentenmodelle diff --git a/langs/en_US/mahnung.lang b/langs/en_US/mahnung.lang index 62079c9..21be5d0 100644 --- a/langs/en_US/mahnung.lang +++ b/langs/en_US/mahnung.lang @@ -346,7 +346,8 @@ MahnungProgProblematisch = Problematic MahnungProgKeineHistorie = no history MahnungProgKeineHistorieTip = Too few paid invoices for a reliable payment forecast MahnungProgSpaeterAlsUeblich = later than usual -MahnungProgTooltip = Expected on %s (on average %s days relative to due date, %s paid invoices) +MahnungProgNachRechnung = d after invoice +MahnungProgTooltip = expected on %s — pays on average %s days after invoice date (%s paid invoices) MahnungVorschlagStufeNichtKonfiguriert = Stage 1 not configured MahnungVorschlagFristNichtErreicht = Stage 1 deadline (%s days) not yet reached (overdue %s days) MahnungVorschlagAlleStufenAusgeschoepft = All dunning stages exhausted (last stage %s)