Fix: Menü-Navigation, GlobalNotify-URL, Dokument-Upload + -Löschen [deploy]
All checks were successful
Deploy mahnung / deploy (push) Successful in 13s

- GlobalNotify-URL: relativer Pfad statt absoluter (DOL_MAIN_URL_ROOT ohne
  Protokoll führte zu kaputten Seiten-URLs im Browser); buildAbsoluteUrl()
  prüft jetzt auf fehlendes http(s)://
- Menü-Navigation: mainmenu=billing&leftmenu=mahnung in allen list.php-Links
  ergänzt (createmahnung.php Redirect, Filter-Form, Hook-Links, Cron-Pfade)
- card.php Dokumente: Lupe direkt neben Dateinamen, Löschen-Button pro Datei
- card.php Upload: actions_linkedfiles.inc.php eingebunden (vor llxHeader),
  upload_dir korrekt gesetzt — showdocuments() zeigt jetzt Upload-Formular
- Redundante Variablen-Definitionen (mahnungSafeRef, files.lib require) entfernt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-05-13 15:36:58 +02:00
parent 5e91c87c99
commit 80d92042bc
8 changed files with 103 additions and 37 deletions

View file

@ -64,7 +64,7 @@ function respond($success, $message, $extra = array())
if (function_exists('setEventMessages')) {
setEventMessages($message, null, $success ? 'mesgs' : 'errors');
}
header('Location: '.DOL_URL_ROOT.'/custom/mahnung/list.php?mode=vorschlag');
header('Location: '.DOL_URL_ROOT.'/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=vorschlag');
exit;
}

View file

@ -79,6 +79,30 @@ if ($action === 'regenerate_pdf' && $user->hasRight('mahnung', 'write')) {
exit;
}
// Generiertes Dokument loeschen
if ($action === 'delete_doc' && $user->hasRight('mahnung', 'write')) {
$docfile = GETPOST('file', 'alphanohtml');
if (!empty($docfile) && !empty($mahnung->fk_facture)) {
$facTmp = new Facture($db);
if ($facTmp->fetch((int) $mahnung->fk_facture) > 0) {
$baseDir = !empty($conf->facture->multidir_output[$facTmp->entity])
? $conf->facture->multidir_output[$facTmp->entity]
: $conf->facture->dir_output;
$fullpath = $baseDir.'/'.dol_sanitizeFileName($facTmp->ref).'/'.dol_sanitizeFileName($docfile);
// Sicherstellen dass die Datei zur Mahnung gehoert (Ref im Dateinamen)
if (file_exists($fullpath) && strpos($docfile, dol_sanitizeFileName($mahnung->ref)) !== false) {
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
dol_delete_file($fullpath);
setEventMessages($langs->trans('FileWasRemoved'), null, 'mesgs');
} else {
setEventMessages('Datei nicht gefunden oder nicht zugehoerig', null, 'errors');
}
}
}
header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id));
exit;
}
// Versand-Daten speichern (Datum, Weg, optional Tracking)
if ($action === 'set_versand' && $user->hasRight('mahnung', 'write')) {
$y = GETPOSTINT('versand_year');
@ -156,13 +180,8 @@ if ($action === 'dismiss_tracking' && $user->hasRight('mahnung', 'write')) {
// 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;
$scanDir = $upload_dir;
$patternService = new MahnungTrackingPattern($db);
$suggestions = array();
@ -225,6 +244,20 @@ if ($action === 'clear_versand' && $user->hasRight('mahnung', 'write')) {
exit;
}
// Upload-Verzeichnis fuer Sendebelege (muss VOR llxHeader stehen fuer actions_linkedfiles)
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
$mahnungSafeRef = dol_sanitizeFileName($mahnung->ref);
$upload_dir = (!empty($conf->mahnung->multidir_output[$mahnung->entity])
? $conf->mahnung->multidir_output[$mahnung->entity]
: $conf->mahnung->dir_output ?? (DOL_DATA_ROOT.'/mahnung'))
.'/'.$mahnungSafeRef;
if (!is_dir($upload_dir)) {
dol_mkdir($upload_dir);
}
$permissiontoadd = $user->hasRight('mahnung', 'write');
$permissiontodelete = $user->hasRight('mahnung', 'write');
include DOL_DOCUMENT_ROOT.'/core/actions_linkedfiles.inc.php';
llxHeader('', $langs->trans('MahnungRef').' '.$mahnung->ref);
print load_fiche_titre($langs->trans('MahnungRef').' '.$mahnung->ref, '', 'fa-envelope-open-text');
@ -265,7 +298,6 @@ print '<br>';
print load_fiche_titre($langs->trans('Documents'), '', 'fa-file');
// Dokumente im Rechnungsordner suchen die zur Mahnung gehoeren
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
$docDir = '';
if ($facture->id > 0) {
$docDir = !empty($conf->facture->multidir_output[$facture->entity])
@ -274,16 +306,16 @@ if ($facture->id > 0) {
$docDir .= '/'.dol_sanitizeFileName($facture->ref);
}
$mahnungRef = dol_sanitizeFileName($mahnung->ref);
$fileList = array();
if (!empty($docDir) && is_dir($docDir)) {
$allFiles = dol_dir_list($docDir, 'files', 0, preg_quote($mahnungRef, '/'));
$allFiles = dol_dir_list($docDir, 'files', 0, preg_quote($mahnungSafeRef, '/'));
foreach ($allFiles as $f) {
$fileList[] = $f;
}
}
if (!empty($fileList)) {
$canDeleteDoc = $user->hasRight('mahnung', 'write');
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('Document').'</th>';
@ -295,30 +327,36 @@ if (!empty($fileList)) {
$fname = $f['name'];
$relativePath = dol_sanitizeFileName($facture->ref).'/'.$fname;
$dlUrl = DOL_URL_ROOT.'/document.php?modulepart=facture&file='.urlencode($relativePath);
$viewUrl = $dlUrl.'&attachment=0'; // Inline-Ansicht (keine Download-Erzwingung)
$viewUrl = $dlUrl.'&attachment=0';
$ext = strtolower(pathinfo($fname, PATHINFO_EXTENSION));
$icon = ($ext === 'pdf') ? 'pdf' : (($ext === 'odt') ? 'ooffice' : 'generic');
$filesize = !empty($f['size']) ? $f['size'] : filesize($f['fullname']);
$filedate = !empty($f['date']) ? $f['date'] : filemtime($f['fullname']);
print '<tr class="oddeven">';
// Dateiname mit Icon
// Dateiname mit Icon + Lupe direkt daneben
print '<td class="nowraponall">';
print '<a href="'.$dlUrl.'">';
print img_picto('', $icon, 'class="pictofixedwidth"');
print dol_escape_htmltag($fname);
print '</a>';
if ($ext === 'pdf') {
print ' <a href="'.$viewUrl.'" target="_blank" title="'.dol_escape_htmltag($langs->trans('Preview')).'">'.img_picto($langs->trans('Preview'), 'search').'</a>';
}
print '</td>';
// Groesse
print '<td class="right nowraponall">'.dol_print_size($filesize, 0, 0).'</td>';
// Datum
print '<td class="center nowraponall">'.dol_print_date($filedate, 'dayhour').'</td>';
// Aktionen: Vorschau + Download
// Aktionen: Download + Loeschen
print '<td class="right nowraponall">';
if ($ext === 'pdf') {
print '<a href="'.$viewUrl.'" target="_blank" title="'.dol_escape_htmltag($langs->trans('Preview')).'">'.img_picto($langs->trans('Preview'), 'search', 'class="pictofixedwidth"').'</a> ';
print '<a href="'.$dlUrl.'">'.img_picto($langs->trans('Download'), 'download').'</a>';
if ($canDeleteDoc) {
$delUrl = $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id).'&action=delete_doc&file='.urlencode($fname).'&token='.newToken();
print ' <a href="'.$delUrl.'" onclick="return confirm(\''.dol_escape_js($langs->trans('MahnungDokumentLoeschenConfirm', $fname)).'\');">';
print img_picto($langs->trans('Delete'), 'delete');
print '</a>';
}
print '<a href="'.$dlUrl.'">'.img_picto($langs->trans('Download'), 'download', 'class="pictofixedwidth"').'</a>';
print '</td>';
print '</tr>';
}
@ -473,21 +511,11 @@ if ($canWrite) {
print '</div>';
}
$mahnungSafeRef = dol_sanitizeFileName($mahnung->ref);
$mahnungFileDir = (!empty($conf->mahnung->multidir_output[$mahnung->entity])
? $conf->mahnung->multidir_output[$mahnung->entity]
: $conf->mahnung->dir_output ?? (DOL_DATA_ROOT.'/mahnung'))
.'/'.$mahnungSafeRef;
// Verzeichnis bei Bedarf anlegen, damit FormFile->showdocuments() das Upload-Formular zeigt
if (!is_dir($mahnungFileDir)) {
dol_mkdir($mahnungFileDir);
}
$urlSelf = $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id);
$formfile->showdocuments(
'mahnung', // $modulepart
$mahnungSafeRef, // $modulesubdir
$mahnungFileDir, // $filedir
$upload_dir, // $filedir
$urlSelf, // $urlsource
0, // $genallowed (kein PDF-Gen-Button hier)
(int) $canWrite, // $delallowed

View file

@ -63,7 +63,7 @@ class ActionsMahnung
$label = $langs->trans('MahnungErstellen');
if ($ueberfaellig) {
$url = DOL_URL_ROOT.'/custom/mahnung/list.php?mode=vorschlag&search_socid='.((int) $object->socid);
$url = DOL_URL_ROOT.'/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=vorschlag&search_socid='.((int) $object->socid);
print dolGetButtonAction($label, '', 'default', $url, 'btn-mahnung-create', 1);
} else {
$attr = array('title' => $langs->trans('MahnungKeineUeberfaelligen'));
@ -114,10 +114,10 @@ class ActionsMahnung
if ($onInvoice) {
$count = $this->countMahnungen($db, 'fk_facture', (int) $object->id);
$tabUrl = DOL_URL_ROOT.'/custom/mahnung/list.php?mode=archiv&fk_facture='.((int) $object->id);
$tabUrl = DOL_URL_ROOT.'/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=archiv&fk_facture='.((int) $object->id);
} else {
$count = $this->countMahnungen($db, 'fk_soc', (int) $object->id);
$tabUrl = DOL_URL_ROOT.'/custom/mahnung/list.php?mode=archiv&search_socid='.((int) $object->id);
$tabUrl = DOL_URL_ROOT.'/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=archiv&search_socid='.((int) $object->id);
}
$head = &$parameters['head'];

View file

@ -74,7 +74,8 @@ class MahnungCron
}
$dolUrl = trim((string) getDolGlobalString('MAIN_INFO_SOCIETE_NOM', ''));
$listUrl = self::buildAbsoluteUrl('/custom/mahnung/list.php?mode=vorschlag');
$relPath = '/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=vorschlag';
$absUrl = self::buildAbsoluteUrl($relPath);
$title = 'Mahnwesen: '.$count.' offene Vorschlaege';
$message = "Stufe 1 (Erinnerung): {$counts[1]}\n";
@ -82,9 +83,10 @@ class MahnungCron
$message .= "Stufe 3 (Letzte Mahnung): {$counts[3]}\n";
$message .= 'Offener Betrag: '.number_format($summe, 2, ',', '.').' EUR';
MahnungNtfy::send($title, $message, $listUrl, array('envelope_with_arrow', 'warning'));
MahnungNtfy::send($title, $message, $absUrl, array('envelope_with_arrow', 'warning'));
// Optional: GlobalNotify-Badge ins Dolibarr-UI (wenn Modul aktiv)
// Relativer Pfad — wird im Browser-Kontext korrekt aufgeloest
if (isModEnabled('globalnotify') && class_exists('GlobalNotify') === false) {
$gnPath = DOL_DOCUMENT_ROOT.'/custom/globalnotify/class/globalnotify.class.php';
if (file_exists($gnPath)) {
@ -96,7 +98,7 @@ class MahnungCron
'mahnung',
'Mahnwesen: '.$count.' Vorschlaege',
$message,
$listUrl,
$relPath,
'Vorschlagsliste oeffnen'
);
}
@ -176,7 +178,8 @@ class MahnungCron
return 0;
}
$listUrl = self::buildAbsoluteUrl('/custom/mahnung/list.php?mode=archiv');
$relPath = '/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=archiv';
$absUrl = self::buildAbsoluteUrl($relPath);
$title = 'Mahnwesen: '.count($pending).' Mahnung(en) unversendet';
$lines = array();
foreach ($pending as $p) {
@ -191,9 +194,9 @@ class MahnungCron
}
$message = implode("\n", $lines);
MahnungNtfy::send($title, $message, $listUrl, array('envelope_with_arrow', 'warning'));
MahnungNtfy::send($title, $message, $absUrl, array('envelope_with_arrow', 'warning'));
// Optional: GlobalNotify-Badge
// Optional: GlobalNotify-Badge — relativer Pfad fuer Browser-Kontext
if (isModEnabled('globalnotify') && !class_exists('GlobalNotify')) {
$gnPath = DOL_DOCUMENT_ROOT.'/custom/globalnotify/class/globalnotify.class.php';
if (file_exists($gnPath)) {
@ -205,7 +208,7 @@ class MahnungCron
'mahnung_versand',
$title,
$message,
$listUrl,
$relPath,
'Archiv oeffnen'
);
}
@ -230,6 +233,10 @@ class MahnungCron
if (empty($base)) {
return $relPath;
}
// Protokoll sicherstellen — ohne Protokoll wird die URL im Browser als relativ interpretiert
if (!preg_match('/^https?:\/\//', $base)) {
$base = 'http://'.$base;
}
return rtrim($base, '/').$relPath;
}
}

View file

@ -95,6 +95,14 @@ class InterfaceMahnungTriggers extends DolibarrTriggers
}
}
$this->db->free($resql);
// Wenn Mahnungen erledigt wurden: pruefen ob noch offene uebrig sind.
// Falls nicht, GlobalNotify-Badge raeumen.
if ($count > 0 && !$this->hatOffeneMahnungen()) {
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungcron.class.php';
MahnungCron::clearGlobalNotify();
}
return $count;
}
@ -128,6 +136,25 @@ class InterfaceMahnungTriggers extends DolibarrTriggers
return $total;
}
/**
* Prueft ob noch offene Mahnvorgaenge existieren (Status weder erledigt noch storniert).
*
* @return bool true wenn mindestens eine offene Mahnung existiert
*/
private function hatOffeneMahnungen()
{
$sql = "SELECT COUNT(*) AS nb FROM ".MAIN_DB_PREFIX."mahnung_mahnung";
$sql .= " WHERE status NOT IN (".Mahnung::STATUS_ERLEDIGT.", ".Mahnung::STATUS_STORNIERT.")";
$resql = $this->db->query($sql);
if (!$resql) {
return true; // Im Zweifel Badge stehen lassen
}
$obj = $this->db->fetch_object($resql);
$this->db->free($resql);
return (int) $obj->nb > 0;
}
/**
* @param int $factureId
* @return bool

View file

@ -211,3 +211,4 @@ MahnungDokumentModelle = Dokumentenmodelle
MahnungPdfStandard = Standard-PDF (DIN 5008)
MahnungGenerate = Dokument generieren
NoDocuments = Keine Dokumente vorhanden.
MahnungDokumentLoeschenConfirm = Dokument '%s' wirklich loeschen?

View file

@ -199,3 +199,4 @@ MahnungCronBuildVorschlag = Dunning — build proposal list
MahnungCronBuildVorschlagDesc = Daily scan for overdue invoices, sends a Ntfy push with the count of new proposals.
MahnungCronVersandReminder = Dunning — shipment reminder (unsent dunnings)
MahnungCronVersandReminderDesc = Daily check for dunnings in status ERSTELLT that have not been sent for more than N days (MAHNUNG_VERSAND_REMINDER_DAYS, default 2).
MahnungDokumentLoeschenConfirm = Really delete document '%s'?

View file

@ -93,6 +93,8 @@ print load_fiche_titre(
// --- Filter-Form ---
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="mainmenu" value="billing">';
print '<input type="hidden" name="leftmenu" value="mahnung">';
print '<input type="hidden" name="mode" value="'.dol_escape_htmltag($mode).'">';
print '<table class="noborder centpercent"><tr class="liste_titre">';
print '<th>'.$langs->trans('MahnungStufe').'</th>';