diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0003eee..77db5b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
## [Unreleased]
+### Beleg-Scan mit Sendungsnummer-Erkennung
+- Neuer Button "Belege scannen" im Versand-Block der Mahnungs-Karte.
+- Beim Klick werden alle hochgeladenen Belege (PDF via `pdftotext`, sonst txt/html) durchsucht und gegen die konfigurierten Tracking-Patterns gematcht.
+- Erkannte Sendungsnummern werden als Vorschlag ueber dem Beleg-Bereich angezeigt (mit Dateiname, Provider-Label, Sendungsnummer + Deep-Link).
+- Per "Uebernehmen"-Button werden `tracking_nr` + `tracking_provider` in einem Klick gespeichert. "Verwerfen" entfernt den Vorschlag aus der Session.
+- Fallback: wenn `pdftotext` im Container fehlt, wird das im UI klar gemeldet — txt/html werden trotzdem durchsucht.
+- OCR (Tesseract fuer Bilder) bewusst noch nicht enthalten — kommt separat falls gewuenscht, ist Container-Aufwand.
+
### Konfigurierbare Tracking-Patterns (Setup-Seite)
- Neue Tabelle `llx_mahnung_trackingpattern` (Pro Eintrag: provider, label, regex, url_template, priority, active). Auto-Migration + Default-Seed beim Setup-Aufruf.
- Default-Patterns: DHL Paket (20-stellig), DPAG Einschreiben (`RR123456789DE`), UPS (1Z…), DHL 11-stellig, Hermes 14-stellig, DPD 14-stellig — Prioritaeten so gesetzt dass spezifischere Patterns zuerst greifen.
diff --git a/card.php b/card.php
index 54b0350..71ef9d0 100644
--- a/card.php
+++ b/card.php
@@ -100,6 +100,88 @@ if ($action === 'set_versand' && $user->hasRight('mahnung', 'write')) {
exit;
}
+// Sendungsnummer aus erkanntem Vorschlag uebernehmen
+if ($action === 'apply_tracking' && $user->hasRight('mahnung', 'write')) {
+ $nr = trim((string) GETPOST('nr', 'alphanohtml'));
+ $prov = GETPOST('provider', 'aZ09');
+ if ($nr !== '' && $prov !== '') {
+ $mahnung->tracking_nr = $nr;
+ $mahnung->tracking_provider = $prov;
+ if ($mahnung->update($user) > 0) {
+ setEventMessages($langs->trans('MahnungTrackingUebernommen'), null, 'mesgs');
+ }
+ }
+ // Vorschlag aus Session entfernen
+ unset($_SESSION['mahnung_tracking_suggestions_'.((int) $mahnung->id)]);
+ header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id));
+ exit;
+}
+
+// Vorschlaege verwerfen
+if ($action === 'dismiss_tracking' && $user->hasRight('mahnung', 'write')) {
+ unset($_SESSION['mahnung_tracking_suggestions_'.((int) $mahnung->id)]);
+ header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id));
+ exit;
+}
+
+// Belege scannen: pdftotext + Pattern-Matching
+if ($action === 'scan_belege' && $user->hasRight('mahnung', 'write')) {
+ require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungtrackingpattern.class.php';
+ require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
+
+ $mahnungSafeRef = dol_sanitizeFileName($mahnung->ref);
+ $scanDir = (!empty($conf->mahnung->multidir_output[$mahnung->entity])
+ ? $conf->mahnung->multidir_output[$mahnung->entity]
+ : ($conf->mahnung->dir_output ?? (DOL_DATA_ROOT.'/mahnung')))
+ .'/'.$mahnungSafeRef;
+
+ $patternService = new MahnungTrackingPattern($db);
+ $suggestions = array();
+ $pdftotextAvailable = null; // einmalig pruefen
+
+ if (is_dir($scanDir)) {
+ foreach (dol_dir_list($scanDir, 'files', 0) as $file) {
+ $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+ $text = '';
+ if ($ext === 'txt' || $ext === 'html' || $ext === 'htm') {
+ $text = @file_get_contents($file['fullname']);
+ if ($ext === 'html' || $ext === 'htm') {
+ $text = strip_tags((string) $text);
+ }
+ } elseif ($ext === 'pdf') {
+ if ($pdftotextAvailable === null) {
+ $check = @shell_exec('command -v pdftotext 2>/dev/null');
+ $pdftotextAvailable = !empty(trim((string) $check));
+ }
+ if ($pdftotextAvailable) {
+ $cmd = 'pdftotext -layout '.escapeshellarg($file['fullname']).' - 2>/dev/null';
+ $text = (string) @shell_exec($cmd);
+ }
+ }
+ if ($text === '') {
+ continue;
+ }
+ $hit = $patternService->detectFromText($text);
+ if ($hit !== null) {
+ $suggestions[] = array(
+ 'file' => $file['name'],
+ 'provider' => $hit['provider'],
+ 'nr' => $hit['nr'],
+ 'url' => $hit['url'],
+ 'label' => $hit['label'],
+ );
+ }
+ }
+ }
+
+ $_SESSION['mahnung_tracking_suggestions_'.((int) $mahnung->id)] = $suggestions;
+ if ($pdftotextAvailable === false) {
+ setEventMessages($langs->trans('MahnungPdftotextMissing'), null, 'warnings');
+ }
+ header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id));
+ exit;
+}
+
// Versand-Daten zuruecksetzen (z.B. Korrekturmoeglichkeit)
if ($action === 'clear_versand' && $user->hasRight('mahnung', 'write')) {
$mahnung->date_versand = null;
@@ -321,6 +403,47 @@ if (!empty($mahnung->date_versand) && $action !== 'edit_versand') {
print '
| '.img_picto('', 'pdf', 'class="pictofixedwidth"').dol_escape_htmltag($sg['file']).' | '; + print ''.dol_escape_htmltag($sg['label']).' | '; + print ''.dol_escape_htmltag($sg['nr']).' | ';
+ print ''.img_picto('', 'fa-external-link-alt').' | '; + print ''; + if ($canWrite) { + $applyUrl = $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id).'&action=apply_tracking' + .'&nr='.urlencode((string) $sg['nr']) + .'&provider='.urlencode((string) $sg['provider']) + .'&token='.newToken(); + print ''.dol_escape_htmltag($langs->trans('MahnungTrackingUebernehmen')).''; + } + print ' | '; + print '