diff --git a/ajax/createmahnung.php b/ajax/createmahnung.php
index 6e312aa..6fbb68c 100644
--- a/ajax/createmahnung.php
+++ b/ajax/createmahnung.php
@@ -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;
}
diff --git a/card.php b/card.php
index 65b1a0f..9ab46f3 100644
--- a/card.php
+++ b/card.php
@@ -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 '
';
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 '
';
print '';
print '| '.$langs->trans('Document').' | ';
@@ -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 '
';
- // Dateiname mit Icon
+ // Dateiname mit Icon + Lupe direkt daneben
print '| ';
print '';
print img_picto('', $icon, 'class="pictofixedwidth"');
print dol_escape_htmltag($fname);
print '';
+ if ($ext === 'pdf') {
+ print ' '.img_picto($langs->trans('Preview'), 'search').'';
+ }
print ' | ';
// Groesse
print ''.dol_print_size($filesize, 0, 0).' | ';
// Datum
print ''.dol_print_date($filedate, 'dayhour').' | ';
- // Aktionen: Vorschau + Download
+ // Aktionen: Download + Loeschen
print '';
- if ($ext === 'pdf') {
- print ''.img_picto($langs->trans('Preview'), 'search', 'class="pictofixedwidth"').' ';
+ print ''.img_picto($langs->trans('Download'), 'download').'';
+ if ($canDeleteDoc) {
+ $delUrl = $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id).'&action=delete_doc&file='.urlencode($fname).'&token='.newToken();
+ print ' ';
+ print img_picto($langs->trans('Delete'), 'delete');
+ print '';
}
- print ''.img_picto($langs->trans('Download'), 'download', 'class="pictofixedwidth"').'';
print ' | ';
print '
';
}
@@ -473,21 +511,11 @@ if ($canWrite) {
print '';
}
-$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
diff --git a/class/actions_mahnung.class.php b/class/actions_mahnung.class.php
index 1422d55..b96fe50 100644
--- a/class/actions_mahnung.class.php
+++ b/class/actions_mahnung.class.php
@@ -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'];
diff --git a/class/mahnungcron.class.php b/class/mahnungcron.class.php
index d83b1c5..733ae58 100644
--- a/class/mahnungcron.class.php
+++ b/class/mahnungcron.class.php
@@ -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;
}
}
diff --git a/core/triggers/interface_99_modMahnung_MahnungTriggers.class.php b/core/triggers/interface_99_modMahnung_MahnungTriggers.class.php
index 81cac88..2a0bd00 100644
--- a/core/triggers/interface_99_modMahnung_MahnungTriggers.class.php
+++ b/core/triggers/interface_99_modMahnung_MahnungTriggers.class.php
@@ -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
diff --git a/langs/de_DE/mahnung.lang b/langs/de_DE/mahnung.lang
index 3cc95dc..007701a 100644
--- a/langs/de_DE/mahnung.lang
+++ b/langs/de_DE/mahnung.lang
@@ -211,3 +211,4 @@ MahnungDokumentModelle = Dokumentenmodelle
MahnungPdfStandard = Standard-PDF (DIN 5008)
MahnungGenerate = Dokument generieren
NoDocuments = Keine Dokumente vorhanden.
+MahnungDokumentLoeschenConfirm = Dokument '%s' wirklich loeschen?
diff --git a/langs/en_US/mahnung.lang b/langs/en_US/mahnung.lang
index 29c7f6c..0bc2550 100644
--- a/langs/en_US/mahnung.lang
+++ b/langs/en_US/mahnung.lang
@@ -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'?
diff --git a/list.php b/list.php
index e4d96ea..208b272 100644
--- a/list.php
+++ b/list.php
@@ -93,6 +93,8 @@ print load_fiche_titre(
// --- Filter-Form ---
print '