Konfigurierbare Tracking-Patterns mit Live-Vorschau [deploy]
Some checks failed
Deploy mahnung / deploy (push) Failing after 4s
Some checks failed
Deploy mahnung / deploy (push) Failing after 4s
Datenmodell: - Neue Tabelle llx_mahnung_trackingpattern (provider, label, regex, url_template, priority, active). Auto-Anlage + Default-Seed im setup.php und in modMahnung::init() — idempotent. Default-Patterns (priority hoeher = spezifischer, zuerst gepruef): - DHL Paket 20-stellig (90), DPAG Einschreiben RR...DE (85), UPS 1Z... (80), - DHL 11-stellig Online-Frankierung (30), Hermes 14-stellig (25), DPD (20). Setup-Seite admin/tracking_patterns.php: - CRUD (Anlegen/Bearbeiten/Aktivieren-Deaktivieren/Loeschen) - Live-Vorschau: Regex + Beispieltext + URL-Template werden waehrend des Tippens (debounce 300ms) via ajax/regex_preview.php ausgewertet. UI zeigt: Regex-Syntax-Status, gefundene Sendungsnummer, vollstaendige Tracking-URL (anklickbar). - Validierung: Delimiter / # ~ Whitelist, https://-Pflicht, {nr}-Platzhalter, max 255 Zeichen Regex. AJAX-Endpoint ajax/regex_preview.php: - ReDoS-Schutz: max 10 KB Sample, pcre.backtrack_limit=100k. - POST-only (mit Setup-Recht), JSON-Response. card.php: - tracking-URL kommt jetzt aus MahnungTrackingPattern::urlFor() (DB-Lookup nach Provider) statt hardcoded Mahnung::trackingUrl() — letztere bleibt als Fallback. Setup-Seite: neuer Button "Tracking-Muster (Regex)" oben rechts. Lang-Keys: 23 neue (de_DE + en_US) fuer Pattern-CRUD + Live-Vorschau. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bb610a7594
commit
56954d68f3
11 changed files with 867 additions and 2 deletions
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### 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.
|
||||
- Neue Setup-Seite `admin/tracking_patterns.php` mit CRUD: Pattern anlegen/bearbeiten/aktivieren-deaktivieren/loeschen.
|
||||
- **Live-Vorschau**: Beim Tippen von Regex/URL/Beispieltext wird via AJAX-Endpoint `ajax/regex_preview.php` direkt gezeigt ob der Regex syntaktisch gueltig ist, was er aus dem Beispieltext matcht und wie die finale Tracking-URL aussieht.
|
||||
- ReDoS-Schutz im AJAX-Endpoint: max 10 KB Sample, `pcre.backtrack_limit=100k`, Whitelist Delimiter `/ # ~`.
|
||||
- `Mahnung::trackingUrl()` (hardcoded Fallback) bleibt — primaer wird `MahnungTrackingPattern::urlFor()` aus DB-Patterns benutzt.
|
||||
- Setup-Page-Link: Button "Tracking-Muster (Regex)" oben rechts auf der Modul-Setup-Seite.
|
||||
|
||||
### Versand & Belege (Mahnungs-Karte)
|
||||
- Neue Felder `date_versand`, `versandweg`, `tracking_nr`, `tracking_provider` an `llx_mahnung_mahnung` — idempotente Migration laeuft beim ersten Setup-Aufruf nach dem Deploy.
|
||||
- Neuer Block "Versand & Belege" auf der Mahnungs-Karte:
|
||||
|
|
|
|||
|
|
@ -58,6 +58,30 @@ if (!$user->admin && !$user->hasRight('mahnung', 'setup')) {
|
|||
// Schema-Migration bei jedem Setup-Aufruf (idempotent — fehlende Spalten ergaenzen)
|
||||
(new modMahnung($db))->migrateVersandFelder();
|
||||
|
||||
// Tracking-Pattern-Tabelle anlegen (falls noch nicht da) + Default-Patterns seeden
|
||||
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungtrackingpattern.class.php';
|
||||
$check = $db->query("SHOW TABLES LIKE '".$db->escape(MAIN_DB_PREFIX.'mahnung_trackingpattern')."'");
|
||||
if ($check && $db->num_rows($check) == 0) {
|
||||
$db->query("CREATE TABLE ".MAIN_DB_PREFIX."mahnung_trackingpattern ("
|
||||
." rowid INTEGER AUTO_INCREMENT PRIMARY KEY,"
|
||||
." entity INTEGER DEFAULT 1 NOT NULL,"
|
||||
." provider VARCHAR(20) NOT NULL,"
|
||||
." label VARCHAR(80) NOT NULL,"
|
||||
." regex VARCHAR(255) NOT NULL,"
|
||||
." url_template VARCHAR(255) NOT NULL,"
|
||||
." priority INTEGER DEFAULT 100,"
|
||||
." active TINYINT DEFAULT 1,"
|
||||
." datec DATETIME,"
|
||||
." tms TIMESTAMP,"
|
||||
." INDEX idx_trackingpattern_active (active, priority),"
|
||||
." INDEX idx_trackingpattern_entity (entity)"
|
||||
.") ENGINE=InnoDB");
|
||||
}
|
||||
if ($check) {
|
||||
$db->free($check);
|
||||
}
|
||||
MahnungTrackingPattern::seedDefaults($db);
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
|
@ -212,7 +236,8 @@ if ($action === 'del' && $user->hasRight('mahnung', 'setup')) {
|
|||
|
||||
llxHeader('', $langs->trans('MahnungSetupPage'));
|
||||
|
||||
print load_fiche_titre($langs->trans('MahnungSetupPage'), '', 'fa-envelope-open-text');
|
||||
$setupExtra = '<a href="tracking_patterns.php" class="button smallpaddingimp">'.dol_escape_htmltag($langs->trans('MahnungTrackingPatternsSetup')).'</a>';
|
||||
print load_fiche_titre($langs->trans('MahnungSetupPage'), $setupExtra, 'fa-envelope-open-text');
|
||||
print '<span class="opacitymedium">'.$langs->trans('MahnungSetupDescription').'</span><br><br>';
|
||||
|
||||
// --- Block: Konstanten -------------------------------------------------------
|
||||
|
|
|
|||
320
admin/tracking_patterns.php
Normal file
320
admin/tracking_patterns.php
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file mahnung/admin/tracking_patterns.php
|
||||
* \ingroup mahnung
|
||||
* \brief Konfigurations-Seite fuer Tracking-Pattern (Regex + URL-Template).
|
||||
* Live-Vorschau via /custom/mahnung/ajax/regex_preview.php.
|
||||
*/
|
||||
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungtrackingpattern.class.php';
|
||||
|
||||
global $langs, $user, $db;
|
||||
$langs->loadLangs(array('admin', 'mahnung@mahnung'));
|
||||
|
||||
if (!$user->admin && !$user->hasRight('mahnung', 'setup')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$rowid = GETPOSTINT('rowid');
|
||||
|
||||
// POST: Speichern (Neu oder Update)
|
||||
if (($action === 'save_new' || $action === 'save_edit') && $user->hasRight('mahnung', 'setup')) {
|
||||
$p = new MahnungTrackingPattern($db);
|
||||
if ($action === 'save_edit' && $rowid > 0) {
|
||||
if ($p->fetch($rowid) <= 0) {
|
||||
setEventMessages('Pattern nicht gefunden', null, 'errors');
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$p->provider = trim(GETPOST('provider', 'aZ09'));
|
||||
$p->label = trim(GETPOST('label', 'alphanohtml'));
|
||||
$p->regex = (string) GETPOST('regex', 'nohtml'); // Regex kann Sonderzeichen enthalten
|
||||
$p->url_template = trim(GETPOST('url_template', 'alphanohtml'));
|
||||
$p->priority = GETPOSTINT('priority');
|
||||
if ($p->priority <= 0) {
|
||||
$p->priority = 100;
|
||||
}
|
||||
$p->active = GETPOST('active', 'int') ? 1 : 0;
|
||||
|
||||
// Basis-Validierung
|
||||
$errors = array();
|
||||
if ($p->provider === '') {
|
||||
$errors[] = $langs->trans('MahnungTrackingPatternProviderRequired');
|
||||
}
|
||||
if ($p->label === '') {
|
||||
$errors[] = $langs->trans('MahnungTrackingPatternLabelRequired');
|
||||
}
|
||||
if (!MahnungTrackingPattern::isValidRegex($p->regex)) {
|
||||
$errors[] = $langs->trans('MahnungTrackingPatternRegexInvalid');
|
||||
}
|
||||
if (strpos($p->url_template, 'https://') !== 0) {
|
||||
$errors[] = $langs->trans('MahnungTrackingPatternUrlMustHttps');
|
||||
}
|
||||
if (strpos($p->url_template, '{nr}') === false) {
|
||||
$errors[] = $langs->trans('MahnungTrackingPatternUrlMissingPlaceholder');
|
||||
}
|
||||
if (!empty($errors)) {
|
||||
setEventMessages('', $errors, 'errors');
|
||||
} else {
|
||||
$ret = ($action === 'save_new') ? $p->create() : $p->update();
|
||||
if ($ret > 0) {
|
||||
setEventMessages($langs->trans('MahnungTrackingPatternSaved'), null, 'mesgs');
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($p->error ?: 'Speichern fehlgeschlagen', null, 'errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POST/GET: Loeschen
|
||||
if ($action === 'delete' && $rowid > 0 && $user->hasRight('mahnung', 'setup')) {
|
||||
$p = new MahnungTrackingPattern($db);
|
||||
if ($p->fetch($rowid) > 0 && $p->delete() > 0) {
|
||||
setEventMessages($langs->trans('MahnungTrackingPatternDeleted'), null, 'mesgs');
|
||||
}
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Toggle active
|
||||
if ($action === 'toggle_active' && $rowid > 0 && $user->hasRight('mahnung', 'setup')) {
|
||||
$p = new MahnungTrackingPattern($db);
|
||||
if ($p->fetch($rowid) > 0) {
|
||||
$p->active = $p->active ? 0 : 1;
|
||||
$p->update();
|
||||
}
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
llxHeader('', $langs->trans('MahnungTrackingPatternsSetup'));
|
||||
|
||||
print load_fiche_titre($langs->trans('MahnungTrackingPatternsSetup'), '<a class="butAction" href="'.DOL_URL_ROOT.'/custom/mahnung/admin/setup.php">'.dol_escape_htmltag($langs->trans('Back')).'</a>', 'fa-route');
|
||||
|
||||
print '<div class="opacitymedium" style="margin-bottom:12px;">'.$langs->trans('MahnungTrackingPatternsIntro').'</div>';
|
||||
|
||||
$service = new MahnungTrackingPattern($db);
|
||||
$all = $service->fetchAll(false);
|
||||
|
||||
// Edit-Modus laden
|
||||
$editP = null;
|
||||
if ($action === 'edit' && $rowid > 0) {
|
||||
$tmp = new MahnungTrackingPattern($db);
|
||||
if ($tmp->fetch($rowid) > 0) {
|
||||
$editP = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
// Liste der bestehenden Patterns
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans('MahnungTrackingPatternLabel').'</th>';
|
||||
print '<th>'.$langs->trans('MahnungTrackingPatternProvider').'</th>';
|
||||
print '<th>'.$langs->trans('MahnungTrackingPatternRegex').'</th>';
|
||||
print '<th>'.$langs->trans('MahnungTrackingPatternUrlTemplate').'</th>';
|
||||
print '<th class="center">'.$langs->trans('MahnungTrackingPatternPriority').'</th>';
|
||||
print '<th class="center">'.$langs->trans('Status').'</th>';
|
||||
print '<th></th>';
|
||||
print '</tr>';
|
||||
if (empty($all)) {
|
||||
print '<tr><td colspan="7" class="opacitymedium center">'.$langs->trans('MahnungTrackingPatternsEmpty').'</td></tr>';
|
||||
}
|
||||
foreach ($all as $p) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.dol_escape_htmltag($p['label']).'</td>';
|
||||
print '<td>'.dol_escape_htmltag($p['provider']).'</td>';
|
||||
print '<td><code>'.dol_escape_htmltag($p['regex']).'</code></td>';
|
||||
print '<td><span class="opacitymedium small">'.dol_escape_htmltag($p['url_template']).'</span></td>';
|
||||
print '<td class="center">'.((int) $p['priority']).'</td>';
|
||||
print '<td class="center">';
|
||||
$toggleLabel = $p['active'] ? $langs->trans('Active') : $langs->trans('Disabled');
|
||||
$toggleColor = $p['active'] ? 'badge-status4' : 'badge-status8';
|
||||
print '<a class="badge '.$toggleColor.'" href="'.$_SERVER['PHP_SELF'].'?action=toggle_active&rowid='.((int) $p['rowid']).'&token='.newToken().'">'.dol_escape_htmltag($toggleLabel).'</a>';
|
||||
print '</td>';
|
||||
print '<td class="right nowrap">';
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&rowid='.((int) $p['rowid']).'#editform">'.img_picto($langs->trans('Edit'), 'edit').'</a> ';
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&rowid='.((int) $p['rowid']).'&token='.newToken().'" onclick="return confirm(\''.dol_escape_js($langs->trans('ConfirmDelete')).'\');">'.img_picto($langs->trans('Delete'), 'delete').'</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
print '</table>';
|
||||
|
||||
// Formular: Neu / Edit
|
||||
$isEdit = ($editP !== null);
|
||||
$formAction = $isEdit ? 'save_edit' : 'save_new';
|
||||
$valProv = $editP ? $editP->provider : '';
|
||||
$valLabel = $editP ? $editP->label : '';
|
||||
$valRegex = $editP ? $editP->regex : '/\\b(\\d{14})\\b/';
|
||||
$valUrl = $editP ? $editP->url_template : 'https://example.com/track?nr={nr}';
|
||||
$valPrio = $editP ? (int) $editP->priority : 100;
|
||||
$valActive = $editP ? (int) $editP->active : 1;
|
||||
|
||||
print '<br><h3 id="editform">'.dol_escape_htmltag($isEdit ? $langs->trans('MahnungTrackingPatternEditTitle') : $langs->trans('MahnungTrackingPatternNewTitle')).'</h3>';
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" id="patternForm">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="'.dol_escape_htmltag($formAction).'">';
|
||||
if ($isEdit) {
|
||||
print '<input type="hidden" name="rowid" value="'.((int) $editP->id).'">';
|
||||
}
|
||||
|
||||
print '<table class="border centpercent">';
|
||||
print '<tr><td class="titlefield">'.$langs->trans('MahnungTrackingPatternLabel').'</td><td>';
|
||||
print '<input type="text" name="label" id="pat_label" value="'.dol_escape_htmltag($valLabel).'" size="50" required>';
|
||||
print '</td></tr>';
|
||||
|
||||
print '<tr><td>'.$langs->trans('MahnungTrackingPatternProvider').'</td><td>';
|
||||
print '<input type="text" name="provider" id="pat_provider" value="'.dol_escape_htmltag($valProv).'" size="20" required ';
|
||||
print 'placeholder="dhl|dpag|dpd|hermes|ups|custom">';
|
||||
print '</td></tr>';
|
||||
|
||||
print '<tr><td>'.$langs->trans('MahnungTrackingPatternRegex').'</td><td>';
|
||||
print '<input type="text" name="regex" id="pat_regex" value="'.dol_escape_htmltag($valRegex).'" size="60" required ';
|
||||
print 'style="font-family:monospace;">';
|
||||
print '<div id="regex_status" class="opacitymedium small" style="margin-top:4px;"> </div>';
|
||||
print '</td></tr>';
|
||||
|
||||
print '<tr><td>'.$langs->trans('MahnungTrackingPatternUrlTemplate').'</td><td>';
|
||||
print '<input type="text" name="url_template" id="pat_url" value="'.dol_escape_htmltag($valUrl).'" size="60" required ';
|
||||
print 'placeholder="https://...?nr={nr}">';
|
||||
print '<div class="opacitymedium small">'.$langs->trans('MahnungTrackingPatternUrlHint').'</div>';
|
||||
print '</td></tr>';
|
||||
|
||||
print '<tr><td>'.$langs->trans('MahnungTrackingPatternPriority').'</td><td>';
|
||||
print '<input type="number" name="priority" value="'.((int) $valPrio).'" size="4"> ';
|
||||
print '<span class="opacitymedium small">'.$langs->trans('MahnungTrackingPatternPriorityHint').'</span>';
|
||||
print '</td></tr>';
|
||||
|
||||
print '<tr><td>'.$langs->trans('Status').'</td><td>';
|
||||
print '<input type="checkbox" name="active" value="1"'.($valActive ? ' checked' : '').'> '.$langs->trans('Active');
|
||||
print '</td></tr>';
|
||||
|
||||
print '<tr><td>'.$langs->trans('MahnungTrackingPatternSample').'</td><td>';
|
||||
print '<textarea id="pat_sample" rows="3" style="width:100%; font-family:monospace;" placeholder="'.dol_escape_htmltag($langs->trans('MahnungTrackingPatternSamplePlaceholder')).'"></textarea>';
|
||||
print '<div id="pat_preview" class="info" style="margin-top:6px; padding:6px; min-height:20px;"> </div>';
|
||||
print '</td></tr>';
|
||||
print '</table>';
|
||||
|
||||
print '<div style="margin-top:8px;">';
|
||||
print '<button type="submit" class="button">'.dol_escape_htmltag($langs->trans('Save')).'</button> ';
|
||||
if ($isEdit) {
|
||||
print '<a class="butActionRefused" href="'.$_SERVER['PHP_SELF'].'">'.dol_escape_htmltag($langs->trans('Cancel')).'</a>';
|
||||
}
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
// Live-Vorschau-JS
|
||||
$ajaxUrl = DOL_URL_ROOT.'/custom/mahnung/ajax/regex_preview.php';
|
||||
$labelInvalid = dol_escape_js($langs->trans('MahnungTrackingPatternRegexInvalid'));
|
||||
$labelValid = dol_escape_js($langs->trans('MahnungTrackingPatternRegexValid'));
|
||||
$labelMatch = dol_escape_js($langs->trans('MahnungTrackingPatternMatch'));
|
||||
$labelNoMatch = dol_escape_js($langs->trans('MahnungTrackingPatternNoMatch'));
|
||||
|
||||
print <<<EOT
|
||||
<script>
|
||||
(function() {
|
||||
var regexEl = document.getElementById('pat_regex');
|
||||
var urlEl = document.getElementById('pat_url');
|
||||
var sampleEl = document.getElementById('pat_sample');
|
||||
var statusEl = document.getElementById('regex_status');
|
||||
var previewEl = document.getElementById('pat_preview');
|
||||
var debounceTimer = null;
|
||||
|
||||
function update() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(function() {
|
||||
var regex = regexEl.value;
|
||||
var sample = sampleEl.value;
|
||||
var url = urlEl.value;
|
||||
if (!regex) {
|
||||
statusEl.textContent = '';
|
||||
previewEl.innerHTML = ' ';
|
||||
return;
|
||||
}
|
||||
var fd = new FormData();
|
||||
fd.append('regex', regex);
|
||||
fd.append('sample', sample);
|
||||
fd.append('url_template', url);
|
||||
fetch('$ajaxUrl', { method: 'POST', body: fd, credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.valid) {
|
||||
statusEl.textContent = '✗ ' + '$labelInvalid' + (data.error ? ' — ' + data.error : '');
|
||||
statusEl.style.color = '#c33';
|
||||
previewEl.innerHTML = ' ';
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = '✓ ' + '$labelValid';
|
||||
statusEl.style.color = '#2a8';
|
||||
if (data.match) {
|
||||
var html = '$labelMatch' + ': <code><strong>' + escapeHtml(data.match) + '</strong></code>';
|
||||
if (data.preview_url) {
|
||||
html += ' → <a href="' + escapeAttr(data.preview_url) + '" target="_blank" rel="noopener">' + escapeHtml(data.preview_url) + '</a>';
|
||||
}
|
||||
previewEl.innerHTML = html;
|
||||
} else if (sample.trim() !== '') {
|
||||
previewEl.innerHTML = '<span class="opacitymedium">$labelNoMatch</span>';
|
||||
} else {
|
||||
previewEl.innerHTML = ' ';
|
||||
}
|
||||
})
|
||||
.catch(function() { statusEl.textContent = '(Preview-Fehler)'; });
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, function(c) {
|
||||
return { '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c];
|
||||
});
|
||||
}
|
||||
function escapeAttr(s) { return escapeHtml(s); }
|
||||
|
||||
if (regexEl && urlEl && sampleEl) {
|
||||
[regexEl, urlEl, sampleEl].forEach(function(el) {
|
||||
el.addEventListener('input', update);
|
||||
el.addEventListener('change', update);
|
||||
});
|
||||
// Initial-Preview falls Sample schon befuellt
|
||||
update();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
EOT;
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
89
ajax/regex_preview.php
Normal file
89
ajax/regex_preview.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* GPL v3 (siehe COPYING).
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file htdocs/custom/mahnung/ajax/regex_preview.php
|
||||
* \ingroup mahnung
|
||||
* \brief AJAX-Endpoint: Live-Vorschau fuer Tracking-Regex auf der Setup-Seite.
|
||||
*
|
||||
* POST: regex (string), sample (string, max 10 KB), url_template (string)
|
||||
* Response (JSON): { valid: bool, match: string|null, preview_url: string|null, error: string|null }
|
||||
*/
|
||||
|
||||
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'].'/main.inc.php';
|
||||
|
||||
global $user, $langs;
|
||||
$langs->load('mahnung@mahnung');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (!$user->admin && !$user->hasRight('mahnung', 'setup')) {
|
||||
echo json_encode(array('valid' => false, 'error' => 'access denied'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// ReDoS-Schutz: Eingaben begrenzen + Zeitlimit
|
||||
$regex = (string) GETPOST('regex', 'nohtml');
|
||||
$sample = (string) GETPOST('sample', 'nohtml');
|
||||
$urlTemplate = (string) GETPOST('url_template', 'alphanohtml');
|
||||
|
||||
if (strlen($regex) > 255) {
|
||||
echo json_encode(array('valid' => false, 'error' => 'regex too long (max 255)'));
|
||||
exit;
|
||||
}
|
||||
if (strlen($sample) > 10240) {
|
||||
$sample = substr($sample, 0, 10240);
|
||||
}
|
||||
|
||||
// Whitelist: Delimiter / # ~
|
||||
if ($regex === '' || !in_array($regex[0], array('/', '#', '~'), true)) {
|
||||
echo json_encode(array('valid' => false, 'error' => 'allowed delimiters: / # ~'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// ReDoS-Schutz via PCRE-Backtrack-Limit (klein halten)
|
||||
$prevBacktrack = ini_get('pcre.backtrack_limit');
|
||||
$prevRecursion = ini_get('pcre.recursion_limit');
|
||||
@ini_set('pcre.backtrack_limit', '100000');
|
||||
@ini_set('pcre.recursion_limit', '10000');
|
||||
|
||||
$matches = array();
|
||||
$ret = @preg_match($regex, '', $matches); // erst leerer String — testet Syntax
|
||||
if ($ret === false) {
|
||||
@ini_set('pcre.backtrack_limit', (string) $prevBacktrack);
|
||||
@ini_set('pcre.recursion_limit', (string) $prevRecursion);
|
||||
$err = preg_last_error_msg();
|
||||
echo json_encode(array('valid' => false, 'error' => 'invalid regex'.($err && $err !== 'No error' ? ': '.$err : '')));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Echtes Sample matchen
|
||||
$match = null;
|
||||
if ($sample !== '') {
|
||||
$ret = @preg_match($regex, $sample, $matches);
|
||||
if ($ret === 1) {
|
||||
$match = !empty($matches[1]) ? $matches[1] : $matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
@ini_set('pcre.backtrack_limit', (string) $prevBacktrack);
|
||||
@ini_set('pcre.recursion_limit', (string) $prevRecursion);
|
||||
|
||||
$previewUrl = null;
|
||||
if ($match !== null && strpos($urlTemplate, 'https://') === 0 && strpos($urlTemplate, '{nr}') !== false) {
|
||||
$previewUrl = str_replace('{nr}', rawurlencode($match), $urlTemplate);
|
||||
}
|
||||
|
||||
echo json_encode(array(
|
||||
'valid' => true,
|
||||
'match' => $match,
|
||||
'preview_url' => $previewUrl,
|
||||
'error' => null,
|
||||
));
|
||||
exit;
|
||||
3
card.php
3
card.php
|
|
@ -251,7 +251,8 @@ if (!empty($mahnung->date_versand) && $action !== 'edit_versand') {
|
|||
.($mahnung->versandweg && isset($versandwege[$mahnung->versandweg]) ? dol_escape_htmltag($versandwege[$mahnung->versandweg]) : dol_escape_htmltag((string) $mahnung->versandweg))
|
||||
.'</td></tr>';
|
||||
if (!empty($mahnung->tracking_nr)) {
|
||||
$trackUrl = Mahnung::trackingUrl((string) $mahnung->tracking_provider, (string) $mahnung->tracking_nr);
|
||||
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungtrackingpattern.class.php';
|
||||
$trackUrl = (new MahnungTrackingPattern($db))->urlFor((string) $mahnung->tracking_provider, (string) $mahnung->tracking_nr);
|
||||
print '<tr><td>'.$langs->trans('MahnungTrackingNr').'</td><td>';
|
||||
print '<code>'.dol_escape_htmltag($mahnung->tracking_nr).'</code>';
|
||||
if (!empty($trackUrl)) {
|
||||
|
|
|
|||
337
class/mahnungtrackingpattern.class.php
Normal file
337
class/mahnungtrackingpattern.class.php
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License, version 3.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file htdocs/custom/mahnung/class/mahnungtrackingpattern.class.php
|
||||
* \ingroup mahnung
|
||||
* \brief CRUD + Lookup fuer konfigurierbare Tracking-Patterns (Regex + URL-Template).
|
||||
*
|
||||
* Patterns werden in llx_mahnung_trackingpattern gehalten. Beim Upload eines
|
||||
* Sendebelegs werden alle aktiven Patterns nach priority DESC durchprobiert;
|
||||
* der erste Match gewinnt.
|
||||
*/
|
||||
|
||||
class MahnungTrackingPattern
|
||||
{
|
||||
/** @var DoliDB */
|
||||
public $db;
|
||||
|
||||
/** @var int */
|
||||
public $id;
|
||||
|
||||
/** @var int */
|
||||
public $entity;
|
||||
|
||||
/** @var string Provider-Slug, z.B. 'dhl', 'dpag', 'hermes', 'dpd', 'ups', 'custom' */
|
||||
public $provider;
|
||||
|
||||
/** @var string Anzeige-Label, z.B. "DHL Paket 20-stellig" */
|
||||
public $label;
|
||||
|
||||
/** @var string Regex inkl. Delimiter, z.B. '/\b(\d{20})\b/' */
|
||||
public $regex;
|
||||
|
||||
/** @var string URL-Template mit Platzhalter {nr}, z.B. 'https://www.dhl.de/...?piececode={nr}' */
|
||||
public $url_template;
|
||||
|
||||
/** @var int hoeher = wichtiger (zuerst geprueft) */
|
||||
public $priority = 100;
|
||||
|
||||
/** @var int 0/1 */
|
||||
public $active = 1;
|
||||
|
||||
/** @var int Unix-Zeit */
|
||||
public $datec;
|
||||
|
||||
/** @var int Unix-Zeit */
|
||||
public $tms;
|
||||
|
||||
/**
|
||||
* @param DoliDB $db
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
global $conf;
|
||||
$this->db = $db;
|
||||
$this->entity = $conf->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern anlegen.
|
||||
*
|
||||
* @return int <0 Fehler, sonst neue rowid
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if (!self::isValidRegex($this->regex)) {
|
||||
$this->error = 'Invalid regex';
|
||||
return -2;
|
||||
}
|
||||
|
||||
$now = dol_now();
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."mahnung_trackingpattern ";
|
||||
$sql .= "(entity, provider, label, regex, url_template, priority, active, datec) VALUES (";
|
||||
$sql .= ((int) $this->entity).",";
|
||||
$sql .= "'".$this->db->escape((string) $this->provider)."',";
|
||||
$sql .= "'".$this->db->escape((string) $this->label)."',";
|
||||
$sql .= "'".$this->db->escape((string) $this->regex)."',";
|
||||
$sql .= "'".$this->db->escape((string) $this->url_template)."',";
|
||||
$sql .= ((int) $this->priority).",";
|
||||
$sql .= ((int) $this->active).",";
|
||||
$sql .= "'".$this->db->idate($now)."'";
|
||||
$sql .= ")";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'mahnung_trackingpattern');
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return int -1 Fehler, 0 nicht gefunden, >0 OK
|
||||
*/
|
||||
public function fetch($id)
|
||||
{
|
||||
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."mahnung_trackingpattern WHERE rowid = ".((int) $id);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
return -1;
|
||||
}
|
||||
if (!$this->db->num_rows($resql)) {
|
||||
$this->db->free($resql);
|
||||
return 0;
|
||||
}
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$this->id = (int) $obj->rowid;
|
||||
$this->entity = (int) $obj->entity;
|
||||
$this->provider = $obj->provider;
|
||||
$this->label = $obj->label;
|
||||
$this->regex = $obj->regex;
|
||||
$this->url_template = $obj->url_template;
|
||||
$this->priority = (int) $obj->priority;
|
||||
$this->active = (int) $obj->active;
|
||||
$this->datec = $this->db->jdate($obj->datec);
|
||||
$this->tms = $this->db->jdate($obj->tms);
|
||||
$this->db->free($resql);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern aktualisieren.
|
||||
*
|
||||
* @return int <0 Fehler, sonst id
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
return -1;
|
||||
}
|
||||
if (!self::isValidRegex($this->regex)) {
|
||||
$this->error = 'Invalid regex';
|
||||
return -2;
|
||||
}
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX."mahnung_trackingpattern SET ";
|
||||
$sql .= "provider = '".$this->db->escape((string) $this->provider)."'";
|
||||
$sql .= ", label = '".$this->db->escape((string) $this->label)."'";
|
||||
$sql .= ", regex = '".$this->db->escape((string) $this->regex)."'";
|
||||
$sql .= ", url_template = '".$this->db->escape((string) $this->url_template)."'";
|
||||
$sql .= ", priority = ".((int) $this->priority);
|
||||
$sql .= ", active = ".((int) $this->active);
|
||||
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern loeschen.
|
||||
*
|
||||
* @return int <0 Fehler, sonst 1
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
return -1;
|
||||
}
|
||||
$resql = $this->db->query("DELETE FROM ".MAIN_DB_PREFIX."mahnung_trackingpattern WHERE rowid = ".((int) $this->id));
|
||||
return $resql ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Patterns laden (sortiert nach priority DESC, label ASC).
|
||||
*
|
||||
* @param bool $onlyActive
|
||||
* @return array<int, array{rowid:int, provider:string, label:string, regex:string, url_template:string, priority:int, active:int}>
|
||||
*/
|
||||
public function fetchAll($onlyActive = false)
|
||||
{
|
||||
$sql = "SELECT rowid, entity, provider, label, regex, url_template, priority, active";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."mahnung_trackingpattern";
|
||||
$sql .= " WHERE entity IN (".getEntity('mahnung').")";
|
||||
if ($onlyActive) {
|
||||
$sql .= " AND active = 1";
|
||||
}
|
||||
$sql .= " ORDER BY priority DESC, label ASC";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
return array();
|
||||
}
|
||||
$out = array();
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$out[] = array(
|
||||
'rowid' => (int) $obj->rowid,
|
||||
'provider' => $obj->provider,
|
||||
'label' => $obj->label,
|
||||
'regex' => $obj->regex,
|
||||
'url_template' => $obj->url_template,
|
||||
'priority' => (int) $obj->priority,
|
||||
'active' => (int) $obj->active,
|
||||
);
|
||||
}
|
||||
$this->db->free($resql);
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht in einem Beispieltext nach der ersten passenden Sendungsnummer.
|
||||
*
|
||||
* @param string $haystack
|
||||
* @return array|null array{provider:string, nr:string, label:string, url:string} oder null
|
||||
*/
|
||||
public function detectFromText($haystack)
|
||||
{
|
||||
$haystack = (string) $haystack;
|
||||
if ($haystack === '') {
|
||||
return null;
|
||||
}
|
||||
foreach ($this->fetchAll(true) as $p) {
|
||||
if (!self::isValidRegex($p['regex'])) {
|
||||
continue;
|
||||
}
|
||||
$matches = array();
|
||||
$ret = @preg_match($p['regex'], $haystack, $matches);
|
||||
if ($ret === 1) {
|
||||
$nr = !empty($matches[1]) ? $matches[1] : $matches[0];
|
||||
return array(
|
||||
'provider' => $p['provider'],
|
||||
'nr' => $nr,
|
||||
'label' => $p['label'],
|
||||
'url' => str_replace('{nr}', rawurlencode($nr), $p['url_template']),
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Robuste Regex-Validierung (kein ReDoS-Schutz, aber Syntax-Check).
|
||||
* Erlaubt nur Delimiter /, #, ~. URL-Template wird hier NICHT geprueft.
|
||||
*
|
||||
* @param string $regex
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValidRegex($regex)
|
||||
{
|
||||
if (!is_string($regex) || strlen($regex) < 3 || strlen($regex) > 255) {
|
||||
return false;
|
||||
}
|
||||
// Erlaubte Delimiter
|
||||
$first = $regex[0];
|
||||
if (!in_array($first, array('/', '#', '~'), true)) {
|
||||
return false;
|
||||
}
|
||||
// @ unterdrueckt Warning, false bei ungueltigem Regex
|
||||
$ret = @preg_match($regex, '');
|
||||
return $ret !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL aus Provider + Sendungsnummer bauen — sucht passendes Pattern aus DB.
|
||||
* Fallback: Mahnung::trackingUrl() (hardcoded URLs).
|
||||
*
|
||||
* @param string $provider
|
||||
* @param string $nr
|
||||
* @return string
|
||||
*/
|
||||
public function urlFor($provider, $nr)
|
||||
{
|
||||
$nr = trim((string) $nr);
|
||||
if ($nr === '') {
|
||||
return '';
|
||||
}
|
||||
// Erstes aktives Pattern mit passendem Provider nutzen
|
||||
foreach ($this->fetchAll(true) as $p) {
|
||||
if ($p['provider'] === $provider) {
|
||||
return str_replace('{nr}', rawurlencode($nr), $p['url_template']);
|
||||
}
|
||||
}
|
||||
// Fallback auf Hardcoded (in Mahnung::trackingUrl)
|
||||
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php';
|
||||
return Mahnung::trackingUrl($provider, $nr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default-Patterns seeden (idempotent).
|
||||
*
|
||||
* @param DoliDB $db
|
||||
* @return int Anzahl eingefuegter Patterns
|
||||
*/
|
||||
public static function seedDefaults($db)
|
||||
{
|
||||
global $conf;
|
||||
$entity = (int) $conf->entity;
|
||||
|
||||
// Anzahl bestehender Patterns pruefen
|
||||
$resql = $db->query("SELECT COUNT(*) AS nb FROM ".MAIN_DB_PREFIX."mahnung_trackingpattern WHERE entity = ".$entity);
|
||||
if (!$resql) {
|
||||
return 0;
|
||||
}
|
||||
$obj = $db->fetch_object($resql);
|
||||
$db->free($resql);
|
||||
if ((int) $obj->nb > 0) {
|
||||
return 0; // Schon befuellt
|
||||
}
|
||||
|
||||
// Defaults — priority hoeher = spezifischer (zuerst gepruef)
|
||||
$defaults = array(
|
||||
array('dhl', 'DHL Paket (20-stellig)', '/\b(\d{20})\b/', 'https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode={nr}', 90),
|
||||
array('dpag', 'Deutsche Post Einschreiben', '/\b(R[A-Z]\d{9}DE)\b/', 'https://www.deutschepost.de/sendung/simpleQuery.html?form.sendungsnummer={nr}', 85),
|
||||
array('ups', 'UPS (1Z + 16 Zeichen)', '/\b(1Z[A-HJ-NP-Z0-9]{16})\b/i', 'https://www.ups.com/track?tracknum={nr}', 80),
|
||||
array('dhl', 'DHL Paket Online-Frankierung (11-stellig)','/\b(\d{11})\b/', 'https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode={nr}', 30),
|
||||
array('hermes', 'Hermes (14-stellig)', '/\b([Hh]?\d{14})\b/', 'https://www.myhermes.de/empfangen/sendungsverfolgung/sendungsinformation/#{nr}', 25),
|
||||
array('dpd', 'DPD (14-stellig)', '/\b(\d{14})\b/', 'https://tracking.dpd.de/status/de_DE/parcel/{nr}', 20),
|
||||
);
|
||||
$now = dol_now();
|
||||
$count = 0;
|
||||
foreach ($defaults as $row) {
|
||||
list($provider, $label, $regex, $url, $priority) = $row;
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."mahnung_trackingpattern ";
|
||||
$sql .= "(entity, provider, label, regex, url_template, priority, active, datec) VALUES (";
|
||||
$sql .= $entity.",";
|
||||
$sql .= "'".$db->escape($provider)."',";
|
||||
$sql .= "'".$db->escape($label)."',";
|
||||
$sql .= "'".$db->escape($regex)."',";
|
||||
$sql .= "'".$db->escape($url)."',";
|
||||
$sql .= ((int) $priority).",";
|
||||
$sql .= "1,";
|
||||
$sql .= "'".$db->idate($now)."'";
|
||||
$sql .= ")";
|
||||
if ($db->query($sql)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
|
@ -319,6 +319,10 @@ class modMahnung extends DolibarrModules
|
|||
// Migration: Versand-Felder ergaenzen, falls Tabelle aus alter Version stammt
|
||||
$this->migrateVersandFelder();
|
||||
|
||||
// Default-Tracking-Patterns seeden (idempotent — nur beim ersten Mal)
|
||||
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungtrackingpattern.class.php';
|
||||
MahnungTrackingPattern::seedDefaults($this->db);
|
||||
|
||||
// Dokumentenmodelle registrieren
|
||||
$sql = array();
|
||||
$sql[] = "DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = 'standard_mahnung' AND type = 'mahnung' AND entity = ".((int) $conf->entity);
|
||||
|
|
|
|||
|
|
@ -88,6 +88,34 @@ MahnungVersandGeleert = Versanddaten zurueckgesetzt
|
|||
MahnungSendebelege = Sendebelege
|
||||
MahnungSendebelegeHint = Hier Beleg von Post/DHL/Fax/Mail hochladen (PDF, Foto). Bleibt am Mahnvorgang fuer spaetere Nachweise.
|
||||
|
||||
#
|
||||
# Tracking-Patterns (Phase 3)
|
||||
#
|
||||
MahnungTrackingPatternsSetup = Tracking-Muster (Regex)
|
||||
MahnungTrackingPatternsIntro = Hier konfigurierst du, welche Regex auf den Text eines hochgeladenen Sendebelegs angewendet werden, um die Sendungsnummer + den passenden Provider automatisch zu erkennen. Reihenfolge: hoechste Prioritaet zuerst.
|
||||
MahnungTrackingPatternsEmpty = Keine Tracking-Muster konfiguriert. Beim ersten Aufruf werden Standard-Muster eingesetzt.
|
||||
MahnungTrackingPatternLabel = Bezeichnung
|
||||
MahnungTrackingPatternProvider = Provider-Schluessel
|
||||
MahnungTrackingPatternRegex = Regex (mit Delimiter / # ~)
|
||||
MahnungTrackingPatternUrlTemplate = URL-Template
|
||||
MahnungTrackingPatternUrlHint = https-URL mit Platzhalter <code>{nr}</code> — wird mit der erkannten Sendungsnummer ersetzt.
|
||||
MahnungTrackingPatternPriority = Prioritaet
|
||||
MahnungTrackingPatternPriorityHint = Hoeher = wird zuerst gepruef (z.B. 90 fuer spezifisch, 20 fuer generisch).
|
||||
MahnungTrackingPatternSample = Beispieltext (Live-Vorschau)
|
||||
MahnungTrackingPatternSamplePlaceholder = Hier ein Beispiel-Beleg-Text einfuegen — Treffer + URL erscheinen live unten.
|
||||
MahnungTrackingPatternMatch = Treffer
|
||||
MahnungTrackingPatternNoMatch = Kein Treffer im Beispieltext.
|
||||
MahnungTrackingPatternRegexValid = Regex syntaktisch gueltig.
|
||||
MahnungTrackingPatternRegexInvalid = Regex ungueltig.
|
||||
MahnungTrackingPatternNewTitle = Neues Muster anlegen
|
||||
MahnungTrackingPatternEditTitle = Muster bearbeiten
|
||||
MahnungTrackingPatternSaved = Muster gespeichert.
|
||||
MahnungTrackingPatternDeleted = Muster geloescht.
|
||||
MahnungTrackingPatternProviderRequired = Provider-Schluessel fehlt.
|
||||
MahnungTrackingPatternLabelRequired = Bezeichnung fehlt.
|
||||
MahnungTrackingPatternUrlMustHttps = URL-Template muss mit https:// beginnen.
|
||||
MahnungTrackingPatternUrlMissingPlaceholder = URL-Template muss den Platzhalter {nr} enthalten.
|
||||
|
||||
#
|
||||
# Liste / Karte
|
||||
#
|
||||
|
|
|
|||
|
|
@ -88,6 +88,34 @@ MahnungVersandGeleert = Shipment data reset
|
|||
MahnungSendebelege = Shipment receipts
|
||||
MahnungSendebelegeHint = Upload receipt from postal carrier/DHL/fax/mail (PDF or photo). Stays attached to the dunning case for later verification.
|
||||
|
||||
#
|
||||
# Tracking patterns (Phase 3)
|
||||
#
|
||||
MahnungTrackingPatternsSetup = Tracking patterns (regex)
|
||||
MahnungTrackingPatternsIntro = Configure which regex is applied to the text of an uploaded shipment receipt to detect tracking number + provider automatically. Higher priority is checked first.
|
||||
MahnungTrackingPatternsEmpty = No tracking patterns configured. Defaults will be seeded on first call.
|
||||
MahnungTrackingPatternLabel = Label
|
||||
MahnungTrackingPatternProvider = Provider slug
|
||||
MahnungTrackingPatternRegex = Regex (delimiters / # ~)
|
||||
MahnungTrackingPatternUrlTemplate = URL template
|
||||
MahnungTrackingPatternUrlHint = https URL with placeholder <code>{nr}</code> — replaced with the detected tracking number.
|
||||
MahnungTrackingPatternPriority = Priority
|
||||
MahnungTrackingPatternPriorityHint = Higher = checked first (e.g. 90 for specific, 20 for generic).
|
||||
MahnungTrackingPatternSample = Sample text (live preview)
|
||||
MahnungTrackingPatternSamplePlaceholder = Paste a sample receipt text here — match + URL appear live below.
|
||||
MahnungTrackingPatternMatch = Match
|
||||
MahnungTrackingPatternNoMatch = No match in sample.
|
||||
MahnungTrackingPatternRegexValid = Regex syntactically valid.
|
||||
MahnungTrackingPatternRegexInvalid = Invalid regex.
|
||||
MahnungTrackingPatternNewTitle = Add new pattern
|
||||
MahnungTrackingPatternEditTitle = Edit pattern
|
||||
MahnungTrackingPatternSaved = Pattern saved.
|
||||
MahnungTrackingPatternDeleted = Pattern deleted.
|
||||
MahnungTrackingPatternProviderRequired = Provider slug is required.
|
||||
MahnungTrackingPatternLabelRequired = Label is required.
|
||||
MahnungTrackingPatternUrlMustHttps = URL template must start with https://.
|
||||
MahnungTrackingPatternUrlMissingPlaceholder = URL template must contain the {nr} placeholder.
|
||||
|
||||
#
|
||||
# List / card
|
||||
#
|
||||
|
|
|
|||
3
sql/llx_mahnung_trackingpattern.key.sql
Normal file
3
sql/llx_mahnung_trackingpattern.key.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- Indizes fuer llx_mahnung_trackingpattern
|
||||
ALTER TABLE llx_mahnung_trackingpattern ADD INDEX idx_trackingpattern_active (active, priority);
|
||||
ALTER TABLE llx_mahnung_trackingpattern ADD INDEX idx_trackingpattern_entity (entity);
|
||||
21
sql/llx_mahnung_trackingpattern.sql
Normal file
21
sql/llx_mahnung_trackingpattern.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-- Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
|
||||
-- Konfigurierbare Tracking-Pattern (Regex + URL-Template) pro Versand-Provider.
|
||||
|
||||
CREATE TABLE llx_mahnung_trackingpattern (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
entity INTEGER DEFAULT 1 NOT NULL,
|
||||
provider VARCHAR(20) NOT NULL,
|
||||
label VARCHAR(80) NOT NULL,
|
||||
regex VARCHAR(255) NOT NULL,
|
||||
url_template VARCHAR(255) NOT NULL,
|
||||
priority INTEGER DEFAULT 100,
|
||||
active TINYINT DEFAULT 1,
|
||||
datec DATETIME,
|
||||
tms TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
Loading…
Reference in a new issue