From 660e91e65d815a2c88587cc61fb3b3022539dbab Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Mon, 11 May 2026 12:08:44 +0200 Subject: [PATCH] Versand-Reminder Cron + Ntfy-Push fuer unversendete Mahnungen [deploy] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Cron-Job MahnungCronVersandReminder (taeglich): - Sucht in llx_mahnung_mahnung Status=ERSTELLT (1) + date_versand IS NULL + datec < NOW() - INTERVAL N DAY. - N steht in der Konstante MAHNUNG_VERSAND_REMINDER_DAYS (Default 2). - Bei Treffern: Ntfy-Push (Topic MAHNUNG_NTFY_TOPIC) mit Titel + Liste der bis zu 8 Mahnungen ("MAHN2026-0042 (Stufe 2, 3 Tage alt) — Kunde"). - Optional GlobalNotify-Badge "mahnung_versand" wenn GlobalNotify aktiv. Modul-Descriptor: - Cronjobs-Array um Reminder ergaenzt (frequency 1d, priority 55, status 1). Lang-Keys: 2x (de_DE + en_US) fuer Cron-Label + Description. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 5 ++ class/mahnungcron.class.php | 111 ++++++++++++++++++++++++++++++ core/modules/modMahnung.class.php | 14 ++++ langs/de_DE/mahnung.lang | 1 + langs/en_US/mahnung.lang | 2 + 5 files changed, 133 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77db5b4..0e4bffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Versand-Reminder (Cron + Ntfy) +- Neuer Cron-Job `MahnungCronVersandReminder` (taeglich): sucht Mahnungen mit Status `ERSTELLT` deren PDF seit > N Tagen erstellt aber noch nicht versendet wurde, schickt Ntfy-Push und (falls aktiv) GlobalNotify-Badge. +- Schwellenwert konfigurierbar via Konstante `MAHNUNG_VERSAND_REMINDER_DAYS` (Default 2). +- Nachricht listet bis zu 8 Mahnungen (Ref + Stufe + Alter in Tagen + Kunde); Rest als "+N weitere". + ### 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. diff --git a/class/mahnungcron.class.php b/class/mahnungcron.class.php index 2961ec9..d83b1c5 100644 --- a/class/mahnungcron.class.php +++ b/class/mahnungcron.class.php @@ -66,6 +66,8 @@ class MahnungCron $summe = round($summe, 2); if ($count === 0) { + // Alte Notifications raeumen — es gibt nichts mehr zu tun + self::clearGlobalNotify(); $this->output = 'Keine ueberfaelligen Rechnungen mit faelliger Mahnung.'; $this->lastresult = 0; return 0; @@ -104,6 +106,115 @@ class MahnungCron return 0; } + /** + * Raeumt alle GlobalNotify-Notifications fuer das Mahnung-Modul auf. + * Wird aufgerufen wenn keine offenen Vorschlaege mehr existieren. + * + * @return void + */ + public static function clearGlobalNotify() + { + if (!isModEnabled('globalnotify')) { + return; + } + if (!class_exists('GlobalNotify')) { + $gnPath = DOL_DOCUMENT_ROOT.'/custom/globalnotify/class/globalnotify.class.php'; + if (!file_exists($gnPath)) { + return; + } + require_once $gnPath; + } + global $db; + $gn = new GlobalNotify($db); + $gn->clearModuleNotifications('mahnung'); + } + + /** + * Versand-Reminder: Mahnungen mit Status ERSTELLT, deren PDF schon + * laenger als N Tage erstellt wurde, aber noch nicht versendet ist, + * werden gesammelt und per Ntfy gepusht. + * + * Schwellenwert konfigurierbar via MAHNUNG_VERSAND_REMINDER_DAYS (Default 2). + * + * @return int 0 bei Erfolg, < 0 bei Fehler + */ + public function versandReminder() + { + require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php'; + + $tageSchwelle = (int) getDolGlobalString('MAHNUNG_VERSAND_REMINDER_DAYS', '2'); + if ($tageSchwelle <= 0) { + $tageSchwelle = 2; + } + + $sql = "SELECT m.rowid, m.ref, m.stufe, m.datec, m.tms, m.fk_facture, m.fk_soc,"; + $sql .= " s.nom AS soc_nom, f.ref AS facture_ref"; + $sql .= " FROM ".MAIN_DB_PREFIX."mahnung_mahnung as m"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = m.fk_soc"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."facture as f ON f.rowid = m.fk_facture"; + $sql .= " WHERE m.status = ".Mahnung::STATUS_ERSTELLT; + $sql .= " AND m.date_versand IS NULL"; + $sql .= " AND m.datec < DATE_SUB(NOW(), INTERVAL ".$tageSchwelle." DAY)"; + $sql .= " ORDER BY m.datec ASC"; + + $resql = $this->db->query($sql); + if (!$resql) { + $this->error = $this->db->lasterror(); + $this->lastresult = -1; + return -1; + } + + $pending = array(); + while ($obj = $this->db->fetch_object($resql)) { + $pending[] = $obj; + } + $this->db->free($resql); + + if (empty($pending)) { + $this->output = 'Keine Mahnungen unversendet > '.$tageSchwelle.' Tage.'; + $this->lastresult = 0; + return 0; + } + + $listUrl = self::buildAbsoluteUrl('/custom/mahnung/list.php?mode=archiv'); + $title = 'Mahnwesen: '.count($pending).' Mahnung(en) unversendet'; + $lines = array(); + foreach ($pending as $p) { + $tage = (int) floor((time() - strtotime((string) $p->datec)) / 86400); + $lines[] = $p->ref.' (Stufe '.$p->stufe.', '.$tage.' Tage alt) — '.$p->soc_nom; + } + // Auf 8 Zeilen kuerzen, Rest als "+N weitere" + if (count($lines) > 8) { + $rest = count($lines) - 8; + $lines = array_slice($lines, 0, 8); + $lines[] = '+ '.$rest.' weitere'; + } + $message = implode("\n", $lines); + + MahnungNtfy::send($title, $message, $listUrl, array('envelope_with_arrow', 'warning')); + + // Optional: GlobalNotify-Badge + if (isModEnabled('globalnotify') && !class_exists('GlobalNotify')) { + $gnPath = DOL_DOCUMENT_ROOT.'/custom/globalnotify/class/globalnotify.class.php'; + if (file_exists($gnPath)) { + require_once $gnPath; + } + } + if (class_exists('GlobalNotify')) { + GlobalNotify::actionRequired( + 'mahnung_versand', + $title, + $message, + $listUrl, + 'Archiv oeffnen' + ); + } + + $this->output = $title.' — '.count($pending).' Eintraege'; + $this->lastresult = count($pending); + return 0; + } + /** * Baut eine absolute URL aus einem relativen Pfad anhand der Dolibarr-URL-Konfig. * diff --git a/core/modules/modMahnung.class.php b/core/modules/modMahnung.class.php index b5ba6fc..aa09982 100644 --- a/core/modules/modMahnung.class.php +++ b/core/modules/modMahnung.class.php @@ -211,6 +211,20 @@ class modMahnung extends DolibarrModules 'test' => 'isModEnabled("mahnung")', 'priority' => 50, ), + 1 => array( + 'label' => 'MahnungCronVersandReminder', + 'jobtype' => 'method', + 'class' => '/mahnung/class/mahnungcron.class.php', + 'objectname' => 'MahnungCron', + 'method' => 'versandReminder', + 'parameters' => '', + 'comment' => 'Erinnert per Ntfy an Mahnungen mit Status ERSTELLT, die seit > N Tagen nicht versendet wurden (MAHNUNG_VERSAND_REMINDER_DAYS, Default 2)', + 'frequency' => 1, + 'unitfrequency' => 86400, + 'status' => 1, + 'test' => 'isModEnabled("mahnung")', + 'priority' => 55, + ), ); // Berechtigungen diff --git a/langs/de_DE/mahnung.lang b/langs/de_DE/mahnung.lang index 516907e..3955e76 100644 --- a/langs/de_DE/mahnung.lang +++ b/langs/de_DE/mahnung.lang @@ -177,6 +177,7 @@ MahnungSettingsSaved = Einstellungen gespeichert. # Cron # MahnungCronBuildVorschlag = Mahnwesen — Vorschlagsliste aufbauen +MahnungCronVersandReminder = Mahnwesen — Versand-Reminder (unversendete Mahnungen) MahnungCronBuildVorschlagDesc = Sucht taeglich ueberfaellige Rechnungen und sendet einen Ntfy-Push mit der Anzahl neuer Vorschlaege. # diff --git a/langs/en_US/mahnung.lang b/langs/en_US/mahnung.lang index 39e9c87..ab96da6 100644 --- a/langs/en_US/mahnung.lang +++ b/langs/en_US/mahnung.lang @@ -178,3 +178,5 @@ MahnungSettingsSaved = Settings saved. # 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).