* * 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/mahnungcron.class.php * \ingroup mahnung * \brief Cron-Job: Vorschlagsliste überfälliger Rechnungen einsammeln, * Ntfy-Push mit Kennzahl an Eddy. */ require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungvorschlag.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungntfy.class.php'; class MahnungCron { /** @var DoliDB */ public $db; /** @var string */ public $error = ''; /** @var string[] */ public $errors = array(); /** @var string */ public $output = ''; /** @var int|string */ public $lastresult = 0; /** * @param DoliDB $db */ public function __construct($db) { $this->db = $db; } /** * Sucht überfällige Rechnungen, ermittelt Vorschläge je Stufe, * sendet Ntfy-Push mit Anzahl je Stufe und Gesamtwert. * * Fehler und Leerstand werden strikt getrennt: nur wenn die Vorschlagsliste * erfolgreich ermittelt wurde UND leer ist, werden die Notifications geräumt. * Bei einem Fehler bleibt alles stehen und der Cron-Job wird als fehlerhaft * markiert (Rückgabe < 0). * * @return int 0 bei Erfolg, < 0 bei Fehler */ public function buildVorschlagsliste() { global $langs; $langs->load('mahnung@mahnung'); $service = new MahnungVorschlag($this->db); $vorschlaege = $service->getVorschlaege(array()); // false = Vorschläge konnten gar nicht ermittelt werden (SQL-Fehler oder // keine Mahnstufe konfiguriert). Dann NICHTS aufräumen — sonst verschwindet // die Notification, obwohl die offenen Vorgänge weiter existieren. if ($vorschlaege === false || !is_array($vorschlaege)) { $detail = (isset($service->error) && $service->error !== '') ? (string) $service->error : ''; if ($detail === '') { $detail = $langs->transnoentities('MahnungCronFehlerUnbekannt'); } $this->error = $langs->transnoentities('MahnungCronVorschlaegeFehler', $detail); $this->errors = (isset($service->errors) && is_array($service->errors)) ? $service->errors : array(); $this->output = $this->error; $this->lastresult = -1; dol_syslog('MahnungCron::buildVorschlagsliste '.$this->error, LOG_ERR); return -1; } $count = count($vorschlaege); // Zähler dynamisch über die tatsächlich konfigurierten Stufen aufbauen. // Stufennummern sind frei wählbar (0 = kostenlose Zahlungserinnerung), // eine feste 1/2/3-Liste wäre falsch. $stufeObj = new MahnungStufe($this->db); $counts = array(); $labels = array(); foreach ($stufeObj->fetchAllActive() as $s) { $counts[(int) $s->stufe] = 0; $labels[(int) $s->stufe] = (string) $s->label; } $summe = 0.0; foreach ($vorschlaege as $v) { $stufe = (int) $v['vorgeschlagene_stufe']; if (!isset($counts[$stufe])) { // Stufe zwischenzeitlich deaktiviert — trotzdem mitzählen statt schlucken $counts[$stufe] = 0; $labels[$stufe] = (string) ($v['vorgeschlagene_stufe_label'] ?? ''); } $counts[$stufe]++; $summe += (float) $v['betrag_offen']; } ksort($counts); $summe = round($summe, 2); if ($count === 0) { // Echter Leerstand: alte Notifications räumen — es gibt nichts mehr zu tun self::clearGlobalNotify(); $this->output = $langs->transnoentities('MahnungCronKeineUeberfaellige'); $this->lastresult = 0; return 0; } $relPath = '/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=vorschlag'; $absUrl = self::buildAbsoluteUrl($relPath); // transnoentities statt trans: der Ntfy-Push ist Klartext, HTML-Entities // (ü etc.) würden dort wörtlich auftauchen. $title = $langs->transnoentities('MahnungCronOffeneVorschlaege', $count); $zeilen = array(); foreach ($counts as $stufe => $anzahl) { $label = (isset($labels[$stufe]) && $labels[$stufe] !== '') ? $labels[$stufe] : $langs->transnoentities('MahnungCronStufeOhneLabel'); $zeilen[] = $langs->transnoentities('MahnungCronStufeAnzahl', $stufe, $label, $anzahl); } $zeilen[] = $langs->transnoentities('MahnungCronOffenerBetrag', number_format($summe, 2, ',', '.')); $message = implode("\n", $zeilen); 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)) { require_once $gnPath; } } if (class_exists('GlobalNotify')) { GlobalNotify::actionRequired( 'mahnung', $langs->trans('MahnungCronVorschlaege', $count), $message, $relPath, $langs->trans('MahnungCronVorschlagslisteOeffnen') ); } $this->output = $title.' — '.$message; $this->lastresult = $count; return 0; } /** * Räumt alle GlobalNotify-Notifications für das Mahnung-Modul auf. * Wird aufgerufen wenn keine offenen Vorschläge 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) { // Fehler: nichts aufräumen, Job als fehlerhaft melden $this->error = $this->db->lasterror(); $this->errors[] = $this->error; $this->output = $this->error; $this->lastresult = -1; dol_syslog('MahnungCron::versandReminder SQL-Fehler: '.$this->error, LOG_ERR); return -1; } $pending = array(); while ($obj = $this->db->fetch_object($resql)) { $pending[] = $obj; } $this->db->free($resql); global $langs; $langs->load('mahnung@mahnung'); if (empty($pending)) { $this->output = $langs->transnoentities('MahnungCronKeineUnversendet', $tageSchwelle); $this->lastresult = 0; return 0; } $relPath = '/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=archiv'; $absUrl = self::buildAbsoluteUrl($relPath); // transnoentities: Ntfy-Push ist Klartext, keine HTML-Entities $title = $langs->transnoentities('MahnungCronUnversendetTitel', count($pending)); $lines = array(); foreach ($pending as $p) { $tage = (int) floor((time() - strtotime((string) $p->datec)) / 86400); $lines[] = $langs->transnoentities('MahnungCronStufeAlter', $p->ref, $p->stufe, $tage, $p->soc_nom); } // Auf 8 Zeilen kürzen, Rest als "+N weitere" if (count($lines) > 8) { $rest = count($lines) - 8; $lines = array_slice($lines, 0, 8); $lines[] = $langs->transnoentities('MahnungCronWeitere', $rest); } $message = implode("\n", $lines); MahnungNtfy::send($title, $message, $absUrl, array('envelope_with_arrow', 'warning')); // Optional: GlobalNotify-Badge — relativer Pfad für Browser-Kontext 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, $relPath, $langs->trans('MahnungCronArchivOeffnen') ); } $this->output = $langs->trans('MahnungCronEintraege', $title, count($pending)); $this->lastresult = count($pending); return 0; } /** * Baut eine absolute URL aus einem relativen Pfad anhand der Dolibarr-URL-Konfig. * * @param string $relPath * @return string */ private static function buildAbsoluteUrl($relPath) { $base = trim((string) getDolGlobalString('DOLIBARR_MAIN_URL_ROOT', '')); if (empty($base) && defined('DOL_MAIN_URL_ROOT')) { $base = DOL_MAIN_URL_ROOT; } 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; } }