Beleg-Scan: Sendungsnummer aus hochgeladenen PDFs erkennen [deploy]
All checks were successful
Deploy mahnung / deploy (push) Successful in 14s
All checks were successful
Deploy mahnung / deploy (push) Successful in 14s
Neuer Button "Belege scannen" im Versand-Block der Mahnungs-Karte: - Action scan_belege durchlaeuft alle Files in DOL_DATA_ROOT/mahnung/<MAHN-Ref>/ - PDFs werden via pdftotext (CLI, mit Verfuegbarkeits-Check) extrahiert - txt/html werden direkt eingelesen (HTML mit strip_tags) - Pro Datei wird MahnungTrackingPattern::detectFromText() angewendet — matched gegen alle aktiven Patterns nach priority DESC - Treffer landen in $_SESSION als Vorschlag (file, provider, nr, url, label) UX: - Vorschlags-Banner mit gruener Linke ueber dem Beleg-Bereich - Pro Vorschlag: Datei-Icon, Pattern-Label, Sendungsnummer als <code>, externer Link zur Sendungsverfolgung, "Uebernehmen"-Button - "Uebernehmen" (action=apply_tracking) speichert tracking_nr + tracking_provider an der Mahnung und leert Session - "Verwerfen" (action=dismiss_tracking) entfernt nur Session-Eintrag Fallback: - Wenn pdftotext nicht im Container verfuegbar: Warnmeldung im UI, txt/html werden trotzdem verarbeitet. OCR fuer Bilder (PNG/JPG) bewusst noch nicht enthalten — separater Schritt mit Container-Anpassung (Tesseract) wenn gewuenscht. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56954d68f3
commit
216c185fb7
4 changed files with 153 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
123
card.php
123
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 '<br><h3>'.$langs->trans('MahnungSendebelege').'</h3>';
|
||||
print '<div class="opacitymedium" style="margin-bottom:8px;">'.$langs->trans('MahnungSendebelegeHint').'</div>';
|
||||
|
||||
// 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 '<div class="info" style="padding:8px; margin-bottom:8px; border-left:3px solid #2a8;">';
|
||||
print '<strong>'.$langs->trans('MahnungTrackingErkannt').'</strong> <span class="opacitymedium">('.count($suggestions).')</span>';
|
||||
print '<table class="noborder" style="margin-top:6px;">';
|
||||
foreach ($suggestions as $sg) {
|
||||
print '<tr>';
|
||||
print '<td class="nowrap">'.img_picto('', 'pdf', 'class="pictofixedwidth"').dol_escape_htmltag($sg['file']).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($sg['label']).'</td>';
|
||||
print '<td><code><strong>'.dol_escape_htmltag($sg['nr']).'</strong></code></td>';
|
||||
print '<td><a href="'.dol_escape_htmltag($sg['url']).'" target="_blank" rel="noopener" class="opacitymedium">'.img_picto('', 'fa-external-link-alt').'</a></td>';
|
||||
print '<td>';
|
||||
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 '<a class="button smallpaddingimp" href="'.$applyUrl.'">'.dol_escape_htmltag($langs->trans('MahnungTrackingUebernehmen')).'</a>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
print '</table>';
|
||||
if ($canWrite) {
|
||||
print '<div style="margin-top:6px;"><a class="opacitymedium" href="'.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id).'&action=dismiss_tracking&token='.newToken().'">'.dol_escape_htmltag($langs->trans('MahnungTrackingVerwerfen')).'</a></div>';
|
||||
}
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Scan-Button (Belege durchsuchen)
|
||||
if ($canWrite) {
|
||||
print '<div style="margin-bottom:8px;">';
|
||||
print '<a class="button smallpaddingimp" href="'.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id).'&action=scan_belege&token='.newToken().'">';
|
||||
print img_picto('', 'fa-search', 'class="pictofixedwidth"').dol_escape_htmltag($langs->trans('MahnungBelegeScannen'));
|
||||
print '</a> ';
|
||||
print '<span class="opacitymedium small">'.$langs->trans('MahnungBelegeScannenHint').'</span>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
$mahnungSafeRef = dol_sanitizeFileName($mahnung->ref);
|
||||
$mahnungFileDir = (!empty($conf->mahnung->multidir_output[$mahnung->entity])
|
||||
? $conf->mahnung->multidir_output[$mahnung->entity]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#
|
||||
|
|
|
|||
Loading…
Reference in a new issue