# 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 - **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. ## 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)