Widget offene Rechnungen: Brutto-Summe zusaetzlich zur Netto-Summe [deploy]
All checks were successful
Deploy mahnung / deploy (push) Successful in 13s

Summenzeile zeigt jetzt Netto und Brutto (aus f.total_ttc der Rechnung,
nicht aus Netto hochgerechnet - damit bleiben Reverse-Charge, Steuer-
befreiung und Kleinunternehmerregelung korrekt). Neue Lang-Keys
MahnungBoxNetto/MahnungBoxBrutto in de_DE + en_US.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-07-10 08:38:17 +02:00
parent 84e9692ffe
commit 7532770599
5 changed files with 21 additions and 4 deletions

View file

@ -27,7 +27,10 @@ Dolibarr Custom-Modul: 3-stufiges Mahnwesen nach BGB §288 + Versand-Tracking +
- `box_mahnung_offen` basiert 1:1 auf `box_factures_imp.php` (Standard-Widget) - `box_mahnung_offen` basiert 1:1 auf `box_factures_imp.php` (Standard-Widget)
- Zeigt ALLE offenen Rechnungen, nicht nur überfällige - Zeigt ALLE offenen Rechnungen, nicht nur überfällige
- Mahnstufe-Badge nur wenn Mahnung existiert, sonst Strich - Mahnstufe-Badge nur wenn Mahnung existiert, sonst Strich
- **Zähler im Kopf**: Der Badge im Widget-Kopf zeigt die tatsächliche Gesamtzahl offener Rechnungen (verlinkt auf die Rechnungsliste). Titel-Lang-Key hat kein `(%s)` mehr — die Zahl steckt im Badge. Der Zähler wird erst NACH der Query gesetzt (wenn `$num` bekannt ist), damit die frühen Return-Pfade (fehlende Rechte / SQL-Fehler) ohne Zähler bleiben.
- **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. - **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`.
## Hooks-Stolperfallen ## 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) - **`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

@ -25,7 +25,7 @@ Version **0.2.0 + Phasen 1-6 (Unreleased)** — UX-Vorschlagsliste, Versand-Erfa
| E-Mail-Versand mit PDF-Anhang | `CMailFile`, Subject/Body mit Platzhaltern | | E-Mail-Versand mit PDF-Anhang | `CMailFile`, Subject/Body mit Platzhaltern |
| Auto-Erledigung bei Zahlungseingang | Trigger `BILL_PAYED` + `PAYMENT_CUSTOMER_CREATE` | | Auto-Erledigung bei Zahlungseingang | Trigger `BILL_PAYED` + `PAYMENT_CUSTOMER_CREATE` |
| Tab "Mahnungen (n)" auf Rechnungs- + Kundenkarte | Hook `completeTabsHead` (filtert auf add/external) | | Tab "Mahnungen (n)" auf Rechnungs- + Kundenkarte | Hook `completeTabsHead` (filtert auf add/external) |
| Widget: Offene Rechnungen mit Mahnstufe | `box_mahnung_offen` | | Widget: Offene Rechnungen mit Mahnstufe (Zähler im Kopf, Zeilenanzahl einstellbar, Summenzeile Netto+Brutto) | `box_mahnung_offen`, Konstante `MAHNUNG_BOX_MAXLINES` |
| **Vorschlagsliste-Filter** (Mahnstufe, Tage Verzug, Mindestbetrag, Kundentyp B2B/B2C, Kunden-Select2 mit Ajax) | `list.php` — Kundentyp-Wechsel filtert das Kunden-Dropdown sofort per Auto-Submit | | **Vorschlagsliste-Filter** (Mahnstufe, Tage Verzug, Mindestbetrag, Kundentyp B2B/B2C, Kunden-Select2 mit Ajax) | `list.php` — Kundentyp-Wechsel filtert das Kunden-Dropdown sofort per Auto-Submit |
| **Kontakt-Spalte** in Vorschlagsliste (Telefon-/Mail-Icons mit Direktlink) | tel:/mailto: aus `llx_societe.phone/email` | | **Kontakt-Spalte** in Vorschlagsliste (Telefon-/Mail-Icons mit Direktlink) | tel:/mailto: aus `llx_societe.phone/email` |
| **Versand-Erfassung** pro Mahnung (Datum, Versandweg, Sendungsnummer, Tracking-Provider) | `card.php` Versand-Block; setzt automatisch Status auf VERSENDET | | **Versand-Erfassung** pro Mahnung (Datum, Versandweg, Sendungsnummer, Tracking-Provider) | `card.php` Versand-Block; setzt automatisch Status auf VERSENDET |
@ -123,6 +123,7 @@ Vollständige Liste: **Setup → Mahnwesen → Verfügbare Template-Variablen**
| `MAHNUNG_ADDON_PDF_ODT_PATH` | `DOL_DATA_ROOT/doctemplates/mahnung` | ODT-Template-Verzeichnis | | `MAHNUNG_ADDON_PDF_ODT_PATH` | `DOL_DATA_ROOT/doctemplates/mahnung` | ODT-Template-Verzeichnis |
| `MAHNUNG_NTFY_TOPIC` | `vk-builds` | Ntfy-Topic für Vorschlags-Push | | `MAHNUNG_NTFY_TOPIC` | `vk-builds` | Ntfy-Topic für Vorschlags-Push |
| `MAHNUNG_VERSAND_REMINDER_DAYS` | `2` | Schwelle (Tage) für Versand-Reminder Ntfy-Push | | `MAHNUNG_VERSAND_REMINDER_DAYS` | `2` | Schwelle (Tage) für Versand-Reminder Ntfy-Push |
| `MAHNUNG_BOX_MAXLINES` | `0` | Widget: max. angezeigte offene Rechnungen (`0` = alle) |
## Datenbank ## Datenbank

View file

@ -240,8 +240,11 @@ class box_mahnung_offen extends ModeleBoxes
} }
if ($num > 0) { if ($num > 0) {
// Summe (wie Original: separate Query ohne LIMIT) // Summe (wie Original: separate Query ohne LIMIT).
$sql2 = "SELECT SUM(f.total_ht) as total_ht"; // Brutto direkt aus f.total_ttc der Rechnung, NICHT aus total_ht
// hochgerechnet — so bleiben Sonderregelungen (Reverse-Charge §13b,
// Steuerbefreiung, Kleinunternehmer §19 UStG) korrekt.
$sql2 = "SELECT SUM(f.total_ht) as total_ht, SUM(f.total_ttc) as total_ttc";
$sql2 .= " FROM ".MAIN_DB_PREFIX."facture as f"; $sql2 .= " FROM ".MAIN_DB_PREFIX."facture as f";
$sql2 .= " INNER JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = f.fk_soc"; $sql2 .= " INNER JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = f.fk_soc";
$sql2 .= " WHERE f.entity IN (".getEntity('invoice').")"; $sql2 .= " WHERE f.entity IN (".getEntity('invoice').")";
@ -251,9 +254,11 @@ class box_mahnung_offen extends ModeleBoxes
} }
$resTotal = $this->db->query($sql2); $resTotal = $this->db->query($sql2);
$totalHt = 0; $totalHt = 0;
$totalTtc = 0;
if ($resTotal) { if ($resTotal) {
$objTotal = $this->db->fetch_object($resTotal); $objTotal = $this->db->fetch_object($resTotal);
$totalHt = (float) $objTotal->total_ht; $totalHt = (float) $objTotal->total_ht;
$totalTtc = (float) $objTotal->total_ttc;
$this->db->free($resTotal); $this->db->free($resTotal);
} }
@ -266,9 +271,13 @@ class box_mahnung_offen extends ModeleBoxes
'td' => 'class="liste_total"', 'td' => 'class="liste_total"',
'text' => '&nbsp;', 'text' => '&nbsp;',
); );
// Netto (wie bisher) + Brutto darunter, Brutto aus f.total_ttc der
// Rechnung (Sonderregelungen wie §13b/§19 UStG bleiben korrekt).
$this->info_box_contents[$line][] = array( $this->info_box_contents[$line][] = array(
'td' => 'class="nowraponall right liste_total"', 'td' => 'class="nowraponall right liste_total"',
'text' => price($totalHt, 0, $langs, 0, -1, -1, $conf->currency), 'text' => '<span class="opacitymedium paddingright small">'.$langs->trans("MahnungBoxNetto").'</span>'.price($totalHt, 0, $langs, 0, -1, -1, $conf->currency)
.'<br><span class="opacitymedium paddingright small">'.$langs->trans("MahnungBoxBrutto").'</span>'.price($totalTtc, 0, $langs, 0, -1, -1, $conf->currency),
'asis' => 1,
); );
$this->info_box_contents[$line][] = array( $this->info_box_contents[$line][] = array(
'td' => 'class="liste_total"', 'td' => 'class="liste_total"',

View file

@ -209,6 +209,8 @@ MahnungBoxKeineOffenenRechnungen = Keine offenen Kundenrechnungen
MahnungBoxMaxLines = Widget: angezeigte offene Rechnungen MahnungBoxMaxLines = Widget: angezeigte offene Rechnungen
MahnungBoxMaxLinesAll = Alle MahnungBoxMaxLinesAll = Alle
MahnungBoxMaxLinesHelp = Wie viele offene Rechnungen das Home-Widget maximal anzeigt. „Alle" = ohne Begrenzung. Der Zähler im Widget-Kopf zeigt immer die tatsächliche Gesamtzahl. MahnungBoxMaxLinesHelp = Wie viele offene Rechnungen das Home-Widget maximal anzeigt. „Alle" = ohne Begrenzung. Der Zähler im Widget-Kopf zeigt immer die tatsächliche Gesamtzahl.
MahnungBoxNetto = Netto
MahnungBoxBrutto = Brutto
# #
# Dokumentenmodelle # Dokumentenmodelle

View file

@ -334,6 +334,8 @@ MahnungBoxKeineOffenenRechnungen = No open customer invoices
MahnungBoxMaxLines = Widget: displayed open invoices MahnungBoxMaxLines = Widget: displayed open invoices
MahnungBoxMaxLinesAll = All MahnungBoxMaxLinesAll = All
MahnungBoxMaxLinesHelp = How many open invoices the home widget shows at most. "All" = no limit. The counter in the widget header always shows the real total. MahnungBoxMaxLinesHelp = How many open invoices the home widget shows at most. "All" = no limit. The counter in the widget header always shows the real total.
MahnungBoxNetto = Net
MahnungBoxBrutto = Gross
MahnungVorschlagStufeNichtKonfiguriert = Stage 1 not configured MahnungVorschlagStufeNichtKonfiguriert = Stage 1 not configured
MahnungVorschlagFristNichtErreicht = Stage 1 deadline (%s days) not yet reached (overdue %s days) MahnungVorschlagFristNichtErreicht = Stage 1 deadline (%s days) not yet reached (overdue %s days)
MahnungVorschlagAlleStufenAusgeschoepft = All dunning stages exhausted (last stage %s) MahnungVorschlagAlleStufenAusgeschoepft = All dunning stages exhausted (last stage %s)