mahnung/CLAUDE.md
Eduard Wisch fdd2b17ab4
All checks were successful
Deploy mahnung / deploy (push) Successful in 13s
Widget-Prognose: intuitive "Ø X T nach Rechnung" + rotes Datum statt Text [deploy]
Eddy-Feedback: Die Differenz-zur-Faelligkeit (z.B. -13.5 T) war unintuitiv
(Kopfrechnen noetig) und "(spaeter als ueblich)" hat die Widget-Spalte
gesprengt.

- Prognosedatum jetzt am RECHNUNGSDATUM verankert: datef + Oe Tage nach
  Rechnungseingang (avg_pay). Das ist die klassische Days-to-Pay-Kennzahl,
  vermeidet Prognosen vor dem Rechnungsdatum und ist direkt lesbar.
- Sichtbare Zahl = "Oe X T nach Rechnung" (positiv) statt Differenz zur
  Faelligkeit. Ampel-Icon bleibt auf der Faelligkeits-Skala (diff) -> weiter
  Paritaet zur Zahlungsverhalten-Box der Kundenkarte.
- "(spaeter als ueblich)" als Text entfernt: bei verstrichener Prognose
  wird stattdessen nur das DATUM rot gefaerbt (Zeile bleibt schmal),
  Erklaerung steckt im Tooltip.
- getZahlprognose liefert zusaetzlich avg_pay; buildPrognoseCell bekommt
  das Rechnungsdatum durchgereicht. Neuer Lang-Key MahnungProgNachRechnung,
  Tooltip umformuliert.

Lokal gegen die Test-DB verifiziert (Datum aus datef+avg_pay, Rot nur bei
verstrichener Prognose, Zukunfts-Rechnung ohne Rot, php -l sauber).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 11:48:08 +02:00

68 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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