All checks were successful
Deploy mahnung / deploy (push) Successful in 13s
Fix nach Eddys Beobachtung: Kunde mit Historie auf der Kundenkarte (BuchhaltungsWidget bewertet ab n>=1) erschien im Widget als "keine Historie", weil MAHNUNG_PROGNOSE_MIN_N auf 3 stand. - MIN_N Default 3 -> 1: Paritaet zur Zahlungsverhalten-Box, die ebenfalls ab der 1. bezahlten Rechnung bewertet. Weiter per Konstante erhoehbar. - "(ueberfaellig)" -> "(spaeter als ueblich)": Das Flag greift wenn das Prognosedatum verstrichen und die Rechnung noch offen ist - das kann schon VOR der formellen Faelligkeit passieren (Fruehzahler). "ueberfaellig" war dafuer das falsche Wort. Neuer Lang-Key MahnungProgSpaeterAlsUeblich. - Prognosedatum + OE-Verzug jetzt aus dem EXAKTEN Mittelwert (Timestamp statt gerundeter Tage), Anzeige mit einer Nachkommastelle -> deckt sich 1:1 mit "Differenz" auf der Kundenkarte (z.B. -3.1 T -> Faelligkeit-3.1). Lokal gegen die Test-DB verifiziert (n=1/n=2 werden gezeigt, Flag nur bei verstrichener Prognose, Zukunfts-Faelligkeit ohne Flag, php -l sauber). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
7 KiB
Markdown
68 lines
7 KiB
Markdown
# 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)
|