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 '

'.$langs->trans('MahnungSendebelege').'

'; print '
'.$langs->trans('MahnungSendebelegeHint').'
'; +// Tracking-Vorschlaege aus Session-Flash (vom Scan) anzeigen +$suggKey = 'mahnung_tracking_suggestions_'.((int) $mahnung->id); +$suggestions = $_SESSION[$suggKey] ?? null; +if (is_array($suggestions) && !empty($suggestions)) { + print '
'; + print ''.$langs->trans('MahnungTrackingErkannt').' ('.count($suggestions).')'; + print ''; + foreach ($suggestions as $sg) { + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } + print '
'.img_picto('', 'pdf', 'class="pictofixedwidth"').dol_escape_htmltag($sg['file']).''.dol_escape_htmltag($sg['label']).''.dol_escape_htmltag($sg['nr']).''.img_picto('', 'fa-external-link-alt').''; + 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 '
'; + if ($canWrite) { + print '
'.dol_escape_htmltag($langs->trans('MahnungTrackingVerwerfen')).'
'; + } + print '
'; +} + +// Scan-Button (Belege durchsuchen) +if ($canWrite) { + print '
'; + print ''; + print img_picto('', 'fa-search', 'class="pictofixedwidth"').dol_escape_htmltag($langs->trans('MahnungBelegeScannen')); + print ' '; + print ''.$langs->trans('MahnungBelegeScannenHint').''; + print '
'; +} + $mahnungSafeRef = dol_sanitizeFileName($mahnung->ref); $mahnungFileDir = (!empty($conf->mahnung->multidir_output[$mahnung->entity]) ? $conf->mahnung->multidir_output[$mahnung->entity] diff --git a/langs/de_DE/mahnung.lang b/langs/de_DE/mahnung.lang index 5b4eb72..516907e 100644 --- a/langs/de_DE/mahnung.lang +++ b/langs/de_DE/mahnung.lang @@ -116,6 +116,17 @@ MahnungTrackingPatternLabelRequired = Bezeichnung fehlt. MahnungTrackingPatternUrlMustHttps = URL-Template muss mit https:// beginnen. MahnungTrackingPatternUrlMissingPlaceholder = URL-Template muss den Platzhalter {nr} enthalten. +# +# Beleg-Scan (Phase 4) +# +MahnungBelegeScannen = Belege scannen +MahnungBelegeScannenHint = Hochgeladene PDFs nach Sendungsnummer durchsuchen (pdftotext + Regex) +MahnungTrackingErkannt = Sendungsnummern erkannt +MahnungTrackingUebernehmen = Uebernehmen +MahnungTrackingUebernommen = Sendungsnummer uebernommen +MahnungTrackingVerwerfen = Vorschlaege verwerfen +MahnungPdftotextMissing = pdftotext nicht im Container verfuegbar — PDFs koennen nicht durchsucht werden. Nur txt/html werden gescannt. + # # Liste / Karte # diff --git a/langs/en_US/mahnung.lang b/langs/en_US/mahnung.lang index ef03c33..39e9c87 100644 --- a/langs/en_US/mahnung.lang +++ b/langs/en_US/mahnung.lang @@ -116,6 +116,17 @@ MahnungTrackingPatternLabelRequired = Label is required. MahnungTrackingPatternUrlMustHttps = URL template must start with https://. MahnungTrackingPatternUrlMissingPlaceholder = URL template must contain the {nr} placeholder. +# +# Receipt scan (Phase 4) +# +MahnungBelegeScannen = Scan receipts +MahnungBelegeScannenHint = Search uploaded PDFs for tracking numbers (pdftotext + regex) +MahnungTrackingErkannt = Tracking numbers detected +MahnungTrackingUebernehmen = Apply +MahnungTrackingUebernommen = Tracking number applied +MahnungTrackingVerwerfen = Dismiss suggestions +MahnungPdftotextMissing = pdftotext not available in container — PDFs cannot be scanned. Only txt/html will be processed. + # # List / card #