# CLAUDE.md — Mahnung-Modul ## Projekt Dolibarr Custom-Modul: 3-stufiges Mahnwesen nach BGB §288 + Versand-Tracking + Forderungsausfall-Workflow. ## Technisches - **numero**: 500038 (NICHT ändern — 500037 ist Eplan) - **Deploy**: nur via Pipeline (`[deploy]` in Commit-Message), NIEMALS manuell kopieren - **Prod-Pfad**: /mnt/appdata/firma/dolibarr-202509/modules/mahnung/ - **Lokal**: Symlink /var/www/dolibarr/custom/mahnung → repo/ - **Test-DB**: dolibarr_test auf 192.168.155.11 - **Forgejo-Repo**: data/mahnung (NICHT data-it/ — historisch, soll bleiben) ## Schema-Migration - `modMahnung::migrateVersandFelder()` läuft automatisch beim Setup-Page-Aufruf - Idempotent via `SHOW COLUMNS LIKE` → fehlende Spalten via `ALTER TABLE ADD COLUMN` - Default-Tracking-Patterns werden via `MahnungTrackingPattern::seedDefaults()` geseedet (Check: `COUNT(*) > 0` → skip) - **Nach Deploy: User muss Setup-Page einmal aufrufen**, sonst fehlen die neuen Spalten ## Dokumentenmodell-System - `commonGenerateDocument()` fügt automatisch `doc_`/`pdf_` Prefix hinzu - DB-Einträge in `llx_document_model.nom` OHNE Prefix speichern - `actions_setmoduleoptions.inc.php` MUSS vor `llxHeader()` stehen (Upload) - ODT-Templates: mahnung_stufe1.odt, mahnung_stufe2.odt, mahnung_stufe3.odt, mahnung.odt (Fallback) ## Widget - `box_mahnung_offen` basiert 1:1 auf `box_factures_imp.php` (Standard-Widget) - Zeigt ALLE offenen Rechnungen, nicht nur überfällige - 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. - **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). ## 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) - Hook-Kontexte: `invoicecard`, `thirdpartycard`, `ordercard` — letztere für Bonitäts-Warnings. ## Filter-Syntax-Stolperfallen - **`$form->select_company($selected, $htmlname, $filter, ...)`**: der `$filter`-Parameter erwartet **USC-Syntax** `(feld:operator:wert)`, NICHT plain SQL. Beispiel B2C: `(s.tva_intra:is:NULL) OR (s.tva_intra:=:'')`. Sonst SQL-Syntax-Error + 500. (KB #602) - **`search_socid=-1`** wird von `select_company` als "nichts ausgewählt" geliefert → im Filter-Check `> 0` statt `!empty()` nutzen. ## Pipeline-Stolperfallen - **`${{ github.event.head_commit.message }}` NIE direkt in `run:`-Skript interpolieren** — bei Sonderzeichen (Klammern, Backticks) bricht Bash. Immer via `env:` durchreichen. (KB #603) - `[deploy]`-Tag im Commit nötig, sonst kein Auto-Deploy. ## Verzugszinsen-Override - `zinssatz_b2c_uebersteuern` / `zinssatz_b2b_uebersteuern` in `llx_mahnung_stufe`: **NULL** = Standard (Basiszins + Aufschlag), **0** = keine Zinsen, **Wert** = fester Prozentsatz - Nicht-versandte Mahnungen (Status ≤ ERSTELLT) werden beim card.php-Aufruf **automatisch neu berechnet** - Setup-Seite zeigt Placeholder mit Standard-Zinssatz + Hilfetext ## Versand & Bonität (Phase 6) - Versand-Felder: `date_versand`, `versandweg`, `tracking_nr`, `tracking_provider` an `llx_mahnung_mahnung` - Tracking-URLs aus DB (`llx_mahnung_trackingpattern`) via `MahnungTrackingPattern::urlFor()`, Fallback: `Mahnung::trackingUrl()` (hardcoded) - Beleg-Upload: `formfile->showdocuments('mahnung', $ref, $filedir, ...)` — `$conf->mahnung->dir_output` wird von Dolibarr automatisch gesetzt (KB #605), kein Custom-Setup nötig - Beleg-Scan: `pdftotext` + `ocrmypdf` (OCR-Fallback für Bild-PDFs) im `90-Dolibarr-Prod-Custom`-Container; Pattern-Match via `MahnungTrackingPattern::detectFromText()` - `pdftotext` gibt `\x0C` (Form-Feed) bei Bild-PDFs zurück — `trim()` mit expliziter Zeichenliste `" \t\n\r\0\x0B\x0C"` nötig - "Übernehmen" setzt `tracking_nr` + `tracking_provider` + `date_versand` + `versandweg` automatisch (kein extra Speichern) - Uneinbringlich-Klassifikation: `Facture::setCanceled($user, CommonInvoice::CLOSECODE_BADDEBT, $note)` → setzt `fk_statut=3` + `close_code='badcustomer'` (KB #606) - Steuer-Modul kompatibel: EÜR ignoriert (liest nur `llx_paiement`), UStVA filtert `fk_statut IN (1,2)` automatisch (KB #607) ## Dolibarr-Versionshinweise - `f.fk_statut` statt `f.statut` (seit Dolibarr 22.x) - `verifCsrf()` existiert nicht — CSRF via `newToken()` + GETPOST('token') - `dol_mkdir()` gibt 0 zurück wenn Verzeichnis bereits existiert (nicht false) - `dol_dir_list()` gibt `fullname` zurück (nicht `fullpath`) - `$form->formconfirm()` unterstützt textarea-Feld via `$formquestion`-Array (KB #609)