* * 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/card.php * \ingroup mahnung * \brief Detailansicht eines einzelnen Mahnvorgangs. */ $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) { die("Include of main fails"); } require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php'; require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php'; require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/modMahnung.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/lib/mahnung_ui.lib.php'; global $langs, $user, $db; // Schema nach einem reinen Datei-Deploy nachziehen — ohne die Spalte // kosten_vorstufen scheitert sonst jedes update() auf dieser Karte. modMahnung::ensureSchema($db); $langs->loadLangs(array('mahnung@mahnung', 'companies', 'bills')); if (!$user->hasRight('mahnung', 'read')) { accessforbidden(); } $id = GETPOSTINT('id'); $action = GETPOST('action', 'aZ09'); // Antwort aus einem formconfirm-Dialog ('yes' | 'no'). Ohne diese Auswertung würde // auch ein "Nein" die bestätigungspflichtige Aktion ausführen. $confirm = GETPOST('confirm', 'alpha'); $mahnung = new Mahnung($db); if ($mahnung->fetch($id) <= 0) { dol_print_error($db, $langs->trans('MahnungNichtGefunden', $id)); exit; } // Externe Benutzer (Kundenkontakte mit eigenem Dolibarr-Zugang) dürfen nur die // Mahnvorgänge ihrer eigenen Firma sehen. Mahnung::fetch() filtert bloß nach Entity — // ohne diese Prüfung genügt das Durchzählen der id, um fremde Forderungen samt // Beträgen zu lesen. if ((int) $user->socid > 0 && (int) $user->socid !== (int) $mahnung->fk_soc) { accessforbidden(); } $canWrite = $user->hasRight('mahnung', 'write'); // --- CSRF-Absicherung aller zustandsändernden Aktionen ---------------------- // Der Core-Check aus main.inc.php greift auf dieser Installation NICHT: in der // conf.php steht $dolibarr_nocsrfcheck=1, zusätzlich ist // MAIN_SECURITY_CSRF_WITH_TOKEN=0. Der komplette Prüfblock ab main.inc.php:333 // wird damit übersprungen. Ohne eigene Prüfung genügt deshalb ein Aufruf wie // card.php?id=42&action=send&… von einer fremden Seite aus, um im eingeloggten // Kontext eine Aktion auszulösen — beim Mailversand ginge die Zahlungserinnerung // raus, ohne dass jemand das Formular je gesehen hat. // // Verglichen wird gegen BEIDE Session-Slots: main.inc.php rollt das Token bei // jedem Seitenaufruf ($_SESSION['token'] = das bisherige $_SESSION['newtoken']). // Ohne MAIN_SECURITY_CSRF_TOKEN_RENEWAL_ON_EACH_CALL sind beide identisch; mit // der Option landet das mitgeschickte Token in 'token', während 'newtoken' schon // neu gewürfelt ist. Eine Prüfung nur gegen 'newtoken' würde bei aktivierter // Option sämtliche Aktionen der Seite blockieren. // hash_equals() statt ===: konstante Laufzeit, kein Timing-Orakel. $tokenGesendet = (string) GETPOST('token', 'alphanohtml'); $tokenOk = ($tokenGesendet !== '' && $tokenGesendet !== 'notrequired'); if ($tokenOk) { $tokenOk = ((!empty($_SESSION['token']) && hash_equals((string) $_SESSION['token'], $tokenGesendet)) || (!empty($_SESSION['newtoken']) && hash_equals((string) $_SESSION['newtoken'], $tokenGesendet))); } // Aktionen mit Seiteneffekt. Reine Anzeige-Actions (edit_versand, confirm_storno, // ask_uneinbringlich) stehen bewusst NICHT in der Liste: die rendern nur ein // Formular bzw. einen Bestätigungsdialog und werden teils über Links ohne Token // aufgerufen. // // Bewusst KEIN zusätzlicher Zwang auf REQUEST_METHOD=POST: $form->formconfirm() // liefert das "Ja" bei aktivem JavaScript per GET aus — $postconfirmas='GET' ist // in html.form.class.php fest verdrahtet, der Dialog navigiert per location.href. // Eine POST-Pflicht würde Storno, Uneinbringlich-Klassifikation und den // Erinnerungsversand komplett lahmlegen. Das Token wird auf beiden Wegen // mitgeschickt (ajax-Dialog: options="token=…", HTML-Fallback: hidden field) und // ist für sich genommen ausreichend — erraten kann es ein Angreifer nicht. $actionsMitToken = array( 'storno', 'regenerate_pdf', 'delete_doc', 'set_versand', 'confirm_uneinbringlich', 'apply_tracking', 'dismiss_tracking', 'scan_belege', 'clear_versand', // Mailformular: 'send' deckt Absenden, Anhang-Upload und Anhang-Entfernen ab — // alle drei posten dasselbe Formular und tragen dessen Token. 'send', // 'presend' rendert zwar nur das Formular, legt dabei aber die Original-Rechnungs- // PDF als Anhang bereit — und erzeugt sie über Facture::generateDocument() neu, // falls sie fehlt. Damit ist es keine reine Anzeige-Action mehr. 'presend', // Core-Dateiaktionen aus core/actions_linkedfiles.inc.php (Sendebelege): das // Include verarbeitet sie selbst und prüft dabei nur $permissiontoadd, kein Token. // Ohne diese Einträge könnte ein untergeschobener GET-Aufruf einen hochgeladenen // Zustellbeleg löschen oder umbenennen. 'confirm_deletefile', 'confirm_updateline', 'renamefile', ); // 'sendit' und 'linkit' (ebenfalls aus actions_linkedfiles.inc.php) hängen an eigenen // Parametern statt an $action und lassen sich über die Action-Liste nicht erfassen. if (GETPOST('sendit', 'alpha') || GETPOST('linkit', 'alpha') || GETPOST('renamefilesave', 'alpha')) { $actionsMitToken[] = $action; } if (in_array($action, $actionsMitToken, true) && !$tokenOk) { setEventMessages($langs->trans('MahnungCsrfTokenUngueltig'), null, 'errors'); header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } // Stufen-Konfiguration laden. Ob es sich um die kostenlose Zahlungserinnerung // handelt, entscheidet ausschließlich das Flag ist_erinnerung — NICHT die // Stufennummer. Eine Erinnerung kostet nichts (keine Mahngebühr, keine Pauschale // nach §288 Abs. 5, keine Verzugszinsen), hat kein eigenes Mahn-PDF und geht // ausschließlich per E-Mail mit der Original-Rechnung im Anhang raus. require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php'; $stufeObj = new MahnungStufe($db); $stufeGeladen = ($stufeObj->fetchByStufe((int) $mahnung->stufe) > 0); // Rohes Flag der Stufe: gilt für die Stufen-KONFIGURATION, nicht für den Vorgang. $stufeIstErinnerung = ($stufeGeladen && $stufeObj->istErinnerung()); // Tatsächlich gespeicherte Kostenbestandteile dieses Vorgangs. // Das Erinnerungs-Flag hängt an der AKTUELLEN Stufenkonfiguration, der Mahnvorgang // selbst hat dafür (noch) kein eigenes Snapshot-Feld. Setzt jemand ist_erinnerung // nachträglich auf eine bereits benutzte Stufe, würden ALTE Vorgänge mit echten // Gebühren rückwirkend als "kostenlos" dargestellt und wären plötzlich per Mail // versendbar — obwohl im längst erzeugten PDF Beträge stehen. $hatGespeicherteKosten = (abs((float) $mahnung->mahngebuehr) > 0.001 || abs((float) $mahnung->pauschale_b2b) > 0.001 || abs((float) $mahnung->verzugszinsen) > 0.001 || abs((float) $mahnung->kosten_vorstufen) > 0.001); // Maßgeblich für die gesamte Karte: als kostenlose Erinnerung gilt ein Vorgang nur, // wenn Stufen-Flag UND gespeicherte Beträge zusammenpassen. Die Zusatzprüfung verengt // die Erkennung ausschließlich — eine echte Erinnerung hat über mahnungBaueVorgang() // immer 0,00 in allen vier Feldern, eine echte Mahnstufe wird dadurch nie fälschlich // zur Erinnerung. Den umgekehrten Fall (Flag wird von einer Stufe wieder entfernt) // kann diese Heuristik nicht abdecken, dafür braucht es ein Snapshot-Feld am Vorgang. $istErinnerung = ($stufeIstErinnerung && !$hatGespeicherteKosten); // Ende des Eskalationspfads datengetrieben bestimmen — es gibt keine feste Obergrenze // bei Stufe 3 mehr. Maßgeblich ist, ob nach der Stufe dieses Vorgangs überhaupt noch // eine aktive Stufe konfiguriert ist. // naechsteStufeNach() liefert null auch dann, wenn die Stufenkonfiguration gar nicht // ladbar ist (SQL-Fehler oder keine aktive Stufe). Deshalb wird zusätzlich geprüft, // dass überhaupt Stufen geladen wurden: ein Konfigurationsfehler darf den praktisch // unwiderruflichen "Uneinbringlich"-Button nicht freischalten. require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungvorschlag.class.php'; $stufenService = new MahnungVorschlag($db); $stufenKonfiguriert = (count($stufenService->getAlleStufen()) > 0); $istLetzteStufe = ($stufenKonfiguriert && $stufenService->naechsteStufeNach((int) $mahnung->stufe) === null); // Verzugszinsen mit der aktuellen Konfiguration neu berechnen — aber NUR solange noch // kein Dokument erzeugt wurde. // Begründung für die Einschränkung: sobald pdf_path gesetzt ist, existiert ein // Mahnschreiben mit festen Beträgen, das beim Kunden liegen kann. Der Block schreibt // bei JEDEM Kartenaufruf; jede Änderung an der Berechnung (z.B. round() -> floor() bei // den Verzugstagen) oder ein nachträglich gepflegter Zinssatz-Override würde // Bestandsdaten rückwirkend verbiegen, bis DB und versendetes PDF auseinanderlaufen. // Status ENTWURF bleibt zusätzlich erlaubt: ein Entwurf ist ausdrücklich noch nicht // final, ein dort erzeugtes PDF ist nur Vorschau und wird vor dem Versand ohnehin neu // gebaut. Bei einer Zahlungserinnerung entfällt die Neuberechnung komplett — die // kostet nichts. $neuberechnungErlaubt = (empty($mahnung->pdf_path) || (int) $mahnung->status === Mahnung::STATUS_ENTWURF); if (!$istErinnerung && $stufeGeladen && $neuberechnungErlaubt && (int) $mahnung->status <= Mahnung::STATUS_ERSTELLT && !empty($mahnung->fk_facture)) { $_basiszins = (float) getDolGlobalString('MAHNUNG_BASISZINS', '1.27'); $_override = $stufeObj->getZinssatzOverride($mahnung->customertype); // Verzugstage: Differenz Mahndatum - Original-Fälligkeit. // floor() statt round(): angefangene Tage zählen nicht mit — konsistent zu // MahnungVorschlag und ajax/createmahnung.php. Mit round() wich die hier // angezeigte Berechnung sonst um bis zu einen Tag von der beim Anlegen ab. $_tageVerzug = 0; if (!empty($mahnung->datec) && !empty($mahnung->date_lim_reglement_alt)) { $_tageVerzug = max(0, (int) floor(($mahnung->datec - $mahnung->date_lim_reglement_alt) / 86400)); } $_neueZinsen = Mahnung::berechneVerzugszinsen( $mahnung->betrag_offen, $_tageVerzug, $mahnung->customertype, $_basiszins, $_override ); if (abs((float) $mahnung->verzugszinsen - $_neueZinsen) > 0.001) { $mahnung->verzugszinsen = $_neueZinsen; $mahnung->basiszins_snapshot = $_basiszins; $mahnung->rechneSumme(); // Nur mit Schreibrecht tatsächlich speichern: ein reiner Leser ruft die Karte // per GET auf und darf dabei keinen DB-Write auslösen. Ohne Recht bleibt die // Neuberechnung rein in-memory und dient nur der Anzeige. if ($canWrite) { $mahnung->update($user); } } } // Stornieren — nur nach ausdrücklicher Bestätigung im formconfirm-Dialog if ($action === 'storno' && $confirm === 'yes' && $user->hasRight('mahnung', 'delete')) { $mahnung->status = Mahnung::STATUS_STORNIERT; if ($mahnung->update($user) > 0) { setEventMessages($langs->trans('MahnungStornieren').' OK', null, 'mesgs'); header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } setEventMessages($mahnung->error, null, 'errors'); } // Dokument neu generieren if ($action === 'regenerate_pdf' && $canWrite) { if ($istErinnerung) { // Zahlungserinnerungen haben kein eigenes Mahn-PDF — Anhang ist die // unveränderte Original-Rechnung. Button ist ausgeblendet, der Aufruf per // URL wird hier zusätzlich abgefangen. setEventMessages($langs->trans('MahnungErinnerungKeinPdf'), null, 'errors'); } else { $selectedModel = GETPOST('model', 'alphanohtml'); $result = $mahnung->generateDocument($selectedModel, $langs); if ($result > 0) { setEventMessages($langs->trans('MahnungDokumentErstellt'), null, 'mesgs'); } else { setEventMessages($langs->trans('MahnungDokumentFehler').': '.$mahnung->error, null, 'errors'); } } header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } // Generiertes Dokument löschen 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($langs->trans('MahnungDateiNichtGefunden'), 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'); $m = GETPOSTINT('versand_month'); $d = GETPOSTINT('versand_day'); $dateVersand = ($y && $m && $d) ? dol_mktime(12, 0, 0, $m, $d, $y) : dol_now(); $weg = GETPOST('versandweg', 'aZ09'); $trackNr = trim((string) GETPOST('tracking_nr', 'alphanohtml')); $trackProv = GETPOST('tracking_provider', 'aZ09'); if (empty($trackProv) && $trackNr !== '') { $trackProv = Mahnung::defaultProviderForWeg($weg); } if ($mahnung->setVersand($user, $dateVersand, $weg, $trackNr ?: null, $trackProv ?: null) > 0) { setEventMessages($langs->trans('MahnungVersandGespeichert'), null, 'mesgs'); } else { setEventMessages($mahnung->error ?: $langs->trans('MahnungFehlerSpeichern'), null, 'errors'); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } // Rechnung als uneinbringlich klassifizieren (close_code='badcustomer') // — endgültiger Schritt nach erfolglosem Mahnverfahren / Vollstreckung. // Zwingend nur mit ausdrücklichem "Ja" aus dem formconfirm-Dialog: der Schritt // ist praktisch unwiderruflich. if ($action === 'confirm_uneinbringlich' && $confirm === 'yes' && $user->hasRight('mahnung', 'delete')) { require_once DOL_DOCUMENT_ROOT.'/core/class/commoninvoice.class.php'; // Der Schritt schreibt auf der RECHNUNG (fk_statut=3, close_code='badcustomer') // und ist buchhalterisch praktisch unwiderruflich. Ein Mahnungs-Recht allein // reicht dafür nicht — wer keine Rechnungen ändern darf, darf auch keine // abschreiben. if (!$user->hasRight('facture', 'creer')) { setEventMessages($langs->trans('MahnungUneinbringlichKeinRechnungsrecht'), null, 'errors'); header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } $note = trim((string) GETPOST('uneinbringlich_note', 'nohtml')); if ($note === '') { $note = $langs->trans('MahnungVerfahrenErfolglos').' '.dol_print_date(dol_now(), 'day'); } $facReload = new Facture($db); if ($facReload->fetch((int) $mahnung->fk_facture) > 0) { // Rechnung auf STATUS_ABANDONED (3) + close_code='badcustomer' setzen $ret = $facReload->setCanceled($user, CommonInvoice::CLOSECODE_BADDEBT, $note); if ($ret > 0) { // Mahnung mitstornieren — Vorgang ist damit fachlich abgeschlossen $mahnung->status = Mahnung::STATUS_STORNIERT; $mahnung->note_private = trim(($mahnung->note_private ? $mahnung->note_private."\n\n" : '').$langs->trans('MahnungUneinbringlichKlassifiziert').' ('.dol_print_date(dol_now(), 'day').'): '.$note); $mahnung->update($user); setEventMessages($langs->trans('MahnungUneinbringlichErfolg'), null, 'mesgs'); } else { setEventMessages($facReload->error ?: $langs->trans('MahnungFehlerKlassifizieren'), null, 'errors'); } } else { setEventMessages($langs->trans('MahnungRechnungNichtLadbar'), null, 'errors'); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } // Sendungsnummer aus erkanntem Vorschlag übernehmen if ($action === 'apply_tracking' && $user->hasRight('mahnung', 'write')) { $nr = trim((string) GETPOST('nr', 'alphanohtml')); $prov = GETPOST('provider', 'aZ09'); if ($nr !== '' && $prov !== '') { $mahnung->tracking_nr = $nr; $mahnung->tracking_provider = $prov; // Versanddaten gleich mitsetzen wenn noch leer if (empty($mahnung->date_versand)) { $mahnung->date_versand = dol_now(); } if (empty($mahnung->versandweg)) { // Provider → passender Versandweg $provToWeg = array('dpag' => 'einschreiben', 'dhl' => 'dhl', 'dpd' => 'dpd', 'hermes' => 'hermes', 'ups' => 'ups'); $mahnung->versandweg = $provToWeg[$prov] ?? 'post'; } if ($mahnung->update($user) > 0) { setEventMessages($langs->trans('MahnungTrackingUebernommen'), null, 'mesgs'); } } // Vorschlag aus Session entfernen unset($_SESSION['mahnung_tracking_suggestions_'.((int) $mahnung->id)]); header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } // Vorschläge verwerfen if ($action === 'dismiss_tracking' && $user->hasRight('mahnung', 'write')) { unset($_SESSION['mahnung_tracking_suggestions_'.((int) $mahnung->id)]); header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } // 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'; $scanRef = 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')) .'/'.$scanRef; $patternService = new MahnungTrackingPattern($db); $suggestions = array(); $pdftotextAvailable = null; $ocrmypdfAvailable = null; if (is_dir($scanDir)) { foreach (dol_dir_list($scanDir, 'files', 0) as $file) { $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $text = ''; if ($ext === 'txt' || $ext === 'html' || $ext === 'htm') { $text = @file_get_contents($file['fullname']); if ($ext === 'html' || $ext === 'htm') { $text = strip_tags((string) $text); } } elseif ($ext === 'pdf') { if ($pdftotextAvailable === null) { $check = @shell_exec('command -v pdftotext 2>/dev/null'); $pdftotextAvailable = !empty(trim((string) $check)); } if ($pdftotextAvailable) { // Erst normales pdftotext versuchen $cmd = 'pdftotext -layout '.escapeshellarg($file['fullname']).' - 2>/dev/null'; $text = trim((string) @shell_exec($cmd), " \t\n\r\0\x0B\x0C"); // Kein Text? → OCR via ocrmypdf (Bild-PDF lesbar machen) if ($text === '') { if ($ocrmypdfAvailable === null) { $check2 = @shell_exec('command -v ocrmypdf 2>/dev/null'); $ocrmypdfAvailable = !empty(trim((string) $check2)); } if ($ocrmypdfAvailable) { $ocrTmp = $file['fullname'].'.ocr.pdf'; $ocrCmd = 'ocrmypdf --skip-text -l deu+eng --jobs 2 ' .escapeshellarg($file['fullname']).' ' .escapeshellarg($ocrTmp).' 2>/dev/null'; @shell_exec($ocrCmd); if (file_exists($ocrTmp)) { $text = trim((string) @shell_exec('pdftotext -layout '.escapeshellarg($ocrTmp).' - 2>/dev/null')); @unlink($ocrTmp); } } } } } if ($text === '') { continue; } $hit = $patternService->detectFromText($text); if ($hit !== null) { $suggestions[] = array( 'file' => $file['name'], 'provider' => $hit['provider'], 'nr' => $hit['nr'], 'url' => $hit['url'], 'label' => $hit['label'], ); } } } $_SESSION['mahnung_tracking_suggestions_'.((int) $mahnung->id)] = $suggestions; if (empty($suggestions)) { setEventMessages($langs->trans('MahnungKeineSendungsnummerErkannt'), null, 'warnings'); } if ($pdftotextAvailable === false) { setEventMessages($langs->trans('MahnungPdftotextMissing'), null, 'warnings'); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } // Versand-Daten zurücksetzen (z.B. Korrekturmöglichkeit) if ($action === 'clear_versand' && $user->hasRight('mahnung', 'write')) { $mahnung->date_versand = null; $mahnung->versandweg = null; $mahnung->tracking_nr = null; $mahnung->tracking_provider = null; // Status nicht zurückdrehen — nur Daten löschen if ($mahnung->update($user) > 0) { setEventMessages($langs->trans('MahnungVersandGeleert'), null, 'mesgs'); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); exit; } // --- Kontext für die Zahlungserinnerung (Empfänger, Rechnung, Anhang) --- // Rechnung und Kunde werden hier einmal geladen und weiter unten nur noch angezeigt. $facture = new Facture($db); $factureGeladen = ($facture->fetch((int) $mahnung->fk_facture) > 0); $societe = new Societe($db); $societeGeladen = ($societe->fetch((int) $mahnung->fk_soc) > 0); $kundeEmail = $societeGeladen ? trim((string) $societe->email) : ''; // Dateiname der Original-Rechnungs-PDF, die als Anhang mitgeht. Ermittelt und bei // Bedarf erzeugt wird sie in mahnungHoleRechnungsPdf(); hier dient der Name nur dem // Hinweistext in der Dokumenten-Sektion, damit auf der Karte sichtbar ist, was bei // einer Zahlungserinnerung statt eines Mahn-PDFs rausgeht. $anhangDatei = ''; if ($factureGeladen) { $anhangDatei = !empty($facture->last_main_doc) ? basename($facture->last_main_doc) : dol_sanitizeFileName($facture->ref).'.pdf'; } // Funktionsbibliothek des Mailversands — liefert unter anderem die Empfängerliste, // die schon für die Frage gebraucht wird, ob überhaupt versendet werden kann. require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/ajax/sendmail.php'; // Versand der Zahlungserinnerung ist nur erlaubt, wenn es wirklich eine Erinnerung // ist, ein Empfänger existiert und das Recht passt. // $istErinnerung schließt Vorgänge mit gespeicherten Gebühren/Zinsen aus: für die liegt // ein Mahnschreiben mit Beträgen vor, das darf nicht als "freundliche Erinnerung" mit // der blanken Original-Rechnung rausgehen. // // Ein bereits versendeter Vorgang bleibt ausdrücklich versendbar: die Mail kann als // unzustellbar zurückkommen, im Spam hängen bleiben oder schlicht an die falsche // Adresse gegangen sein. Gesperrt sind nur erledigte (= bezahlte) und stornierte // Vorgänge — die prüft zusätzlich mahnungVersandErlaubt(). // // Empfänger: nicht nur die Firmenadresse, auch ein Ansprechpartner mit Mailadresse // genügt; im Formular lässt sich außerdem eine beliebige Adresse eintragen. $erinnerungEmpfaengerMoeglich = ($kundeEmail !== ''); if (!$erinnerungEmpfaengerMoeglich && $societeGeladen) { foreach (mahnungEmpfaengerListe($societe) as $kandidat) { if (mahnungAdresseNormieren($kandidat) !== null) { $erinnerungEmpfaengerMoeglich = true; break; } } } $canSendErinnerung = ($istErinnerung && (int) $mahnung->status !== Mahnung::STATUS_ERLEDIGT && (int) $mahnung->status !== Mahnung::STATUS_STORNIERT && $erinnerungEmpfaengerMoeglich && $user->hasRight('mahnung', 'send')); // Erneuter Versand? Wird ausschließlich aus dem Status abgeleitet, nicht aus einem // Request-Parameter — so kann niemand den Doppelversand-Schutz per URL aushebeln. $istErneuterVersand = ($canSendErinnerung && (int) $mahnung->status >= Mahnung::STATUS_VERSENDET); // --------------------------------------------------------------------------- // Mailformular für die Zahlungserinnerung (Dolibarr-Standard FormMail) // --------------------------------------------------------------------------- // Verschickt wird über das gewohnte Dolibarr-Mailformular: Empfänger (inklusive // aller Ansprechpartner des Kunden), Betreff, Text und Anhang stehen sichtbar im // Formular und sind vor dem Absenden änderbar. Der Versand selbst läuft // ausschließlich über mahnungSendeErinnerungsMail() — dort hängen Statuswächter, // die fachliche Sperre (nur Erinnerungsstufen) und die atomare Reservierung gegen // Doppelversand. ajax/sendmail.php stellt diese Bausteine nur noch bereit, es ist // kein eigener Endpoint mehr. require_once DOL_DOCUMENT_ROOT.'/core/class/html.formmail.class.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; // Die Anhangsliste liegt in der Session; der trackid trennt sie pro Mahnvorgang, // sonst sähe eine zweite offene Karte die Anhänge der ersten. $mahnungTrackId = 'mah'.((int) $mahnung->id); $formmail = new FormMail($db); $formmail->trackid = $mahnungTrackId; // Merker "für diesen Vorgang wurde die Anhangsliste schon einmal vorbelegt". // Er wird zusammen mit der Liste geleert, damit ein später neu geöffnetes Formular // die Rechnung wieder eingehängt bekommt. $mahnungInitMarker = 'mahnung_mailinit_'.((int) $mahnung->id); $urlKarte = $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id); // "Abbrechen" im Mailformular: hochgeladene Anhänge verwerfen, zurück zur Karte. if ($action === 'send' && GETPOST('cancel', 'alpha')) { $formmail->clear_attached_files(); unset($_SESSION[$mahnungInitMarker]); header('Location: '.$urlKarte); exit; } // Anhang hinzufügen / entfernen — gleiches Muster wie core/actions_sendmails.inc.php. // Beide Buttons posten dasselbe Formular (action=send); danach wird das Formular // erneut angezeigt und NICHT gesendet. // // $canSendErinnerung ist Pflicht: ohne die Prüfung könnte jeder eingeloggte Benutzer // (das Leserecht ist Default-Recht) mit einem gültigen Token Dateien in sein // Temp-Verzeichnis hochladen, obwohl er hier gar nichts versenden darf. if ($action === 'send' && (GETPOST('addfile', 'alpha') || GETPOST('removedfile'))) { if (!$canSendErinnerung) { setEventMessages($langs->trans('MahnungMailNichtErlaubt'), null, 'errors'); header('Location: '.$urlKarte); exit; } // Temp-Verzeichnis pro Mahnvorgang: dol_add_file_process() speichert mit // allowoverwrite, ein gleichnamiger Anhang eines anderen Vorgangs würde sonst // überschrieben — und beide Vorgänge zeigten anschließend auf dieselbe Datei. $uploadTmpDir = $conf->user->dir_output.'/'.((int) $user->id).'/temp/'.$mahnungTrackId; if (GETPOST('addfile', 'alpha')) { dol_add_file_process($uploadTmpDir, 1, 0, 'addedfile', '', null, $mahnungTrackId, 0); } else { // Nur aus der Liste nehmen, Datei NICHT von der Platte löschen: der // Standardanhang ist die echte Original-Rechnungs-PDF im Dokumentenordner. dol_remove_file_process(GETPOSTINT('removedfile'), 0, 1, $mahnungTrackId); } $action = 'presend'; } // --- Absenden ------------------------------------------------------------- if ($action === 'send' && $tokenOk) { $sendFehler = ''; // Recht, Erinnerungs-Flag, Status und Kunden-Mailadresse hängen alle an // $canSendErinnerung. Die feinkörnige Begründung liefert zusätzlich // mahnungVersandErlaubt() innerhalb von mahnungSendeErinnerungsMail(). if (!$canSendErinnerung) { $sendFehler = $langs->trans('MahnungMailNichtErlaubt'); } elseif (!$factureGeladen || !$societeGeladen) { $sendFehler = $langs->trans('MahnungRechnungNichtLadbar'); } // --- Empfänger --------------------------------------------------------- // Aus der Auswahlliste kommen nur Schlüssel; die zugehörige Adresse wird über // mahnungEmpfaengerAdresse() aufgelöst. Vorher wird geprüft, dass der Schlüssel // wirklich aus der Liste DIESES Kunden stammt — Societe::contact_get_property() // validiert die Firmenzugehörigkeit nicht, ein manipuliertes receiver[] könnte // die Erinnerung sonst an einen fremden Kontakt schicken. $adressen = array(); if ($sendFehler === '') { $empfaengerListe = mahnungEmpfaengerListe($societe); foreach (GETPOST('receiver', 'array') as $key) { if (!array_key_exists((string) $key, $empfaengerListe) && !array_key_exists((int) $key, $empfaengerListe)) { continue; } // Über dieselbe Normierung wie der Freitextpfad schlüsseln (kleingeschriebene // Mailadresse). Der rohe String "Name " als Schlüssel würde denselben // Empfänger zweimal in den To-Header lassen, sobald er einmal ausgewählt und // einmal von Hand eingetippt wurde. $norm = mahnungAdresseNormieren(mahnungEmpfaengerAdresse($societe, $key)); if ($norm !== null) { $adressen[$norm['mail']] = $norm['voll']; } } // Zusätzlich frei eingetragene Adressen (Komma oder Semikolon getrennt). $verworfen = array(); foreach (mahnungAdressenAusFreitext(GETPOST('sendto', 'alphawithlgt'), $verworfen) as $mail => $voll) { $adressen[$mail] = $voll; } if (!empty($verworfen)) { // Nicht kommentarlos weniger Empfänger beliefern als im Formular standen. $sendFehler = $langs->trans('MahnungMailEmpfaengerUngueltig', implode(', ', $verworfen)); } if ($sendFehler === '' && empty($adressen)) { $sendFehler = $langs->trans('MahnungMailKeinEmpfaenger'); } } // Kopie / Blindkopie — gleiche Prüfung, und wer schon im An-Feld steht, bekommt // die Mail nicht zusätzlich als Kopie. $adressenCc = array(); $adressenBcc = array(); if ($sendFehler === '') { foreach (array('sendtocc' => 'cc', 'sendtoccc' => 'bcc') as $feld => $ziel) { $verworfenKopie = array(); $liste = mahnungAdressenAusFreitext(GETPOST($feld, 'alphawithlgt'), $verworfenKopie); if (!empty($verworfenKopie)) { $sendFehler = $langs->trans('MahnungMailEmpfaengerUngueltig', implode(', ', $verworfenKopie)); break; } foreach ($liste as $mail => $voll) { if (isset($adressen[$mail])) { continue; } if ($ziel === 'cc') { $adressenCc[$mail] = $voll; } else { $adressenBcc[$mail] = $voll; } } } } // --- Rechnung zum Versandzeitpunkt prüfen ------------------------------ // Zwischen Anlage und Versand kann bezahlt, teilbezahlt, storniert oder // abgeschrieben worden sein. Ändert sich der offene Betrag, zieht die Prüfung // betrag_offen/summe_mahnung am Objekt nach, damit Mail und Datensatz denselben // Betrag nennen. $betragAngepasst = false; if ($sendFehler === '') { $pruefFehler = ''; if (!mahnungRechnungNochOffen($mahnung, $facture, $pruefFehler, $betragAngepasst)) { $sendFehler = $pruefFehler; } elseif ($betragAngepasst) { // Der Kunde hat zwischen dem Öffnen des Formulars und dem Klick auf "Senden" // eine Teilzahlung geleistet. Im Textfeld steht noch der alte Betrag — die // Platzhalter wurden beim Aufbau des Formulars bereits ersetzt, ein erneutes // Ersetzen findet keine Marke mehr. Statt eine Mail mit falschem Betrag zu // verschicken, wird zurück ins Formular geleitet: es baut Betreff und Text // mit dem jetzt gültigen Betrag neu auf, und Eddy schickt bewusst ab. $sendFehler = $langs->trans('MahnungMailBetragAngepasst', price($mahnung->summe_mahnung)); // Die geposteten Werte verwerfen: FormMail zeigt ein vorhandenes subject/message // bevorzugt an und würde sonst genau den veralteten Text wieder einsetzen. unset($_POST['subject'], $_POST['message'], $_GET['subject'], $_GET['message']); } } // --- Absender ---------------------------------------------------------- // Bewusst die Firmenadresse, nicht die des angemeldeten Benutzers: eine // Zahlungserinnerung geht im Namen des Betriebs raus. Ein trotzdem geposteter // frommail-Wert wird ignoriert. $absender = array('voll' => ''); if ($sendFehler === '') { $absenderFehler = ''; $absender = mahnungAbsender($absenderFehler); if ($absenderFehler !== '') { $sendFehler = $absenderFehler; } } // --- Senden ------------------------------------------------------------ if ($sendFehler === '') { $anhaenge = $formmail->get_attached_files(); $neueFrist = mahnungBerechneNeueFrist($stufeObj, dol_now()); $werte = mahnungPlatzhalterWerte($mahnung, $facture, $societe, $neueFrist, $stufeObj); // Betreff und Text kommen aus dem Formular. Platzhalter werden noch einmal // aufgelöst — so wirken auch {…}-Marken, die im Formular ergänzt wurden. // // mahnungBodyEntkleiden() macht die nl2br-Umwandlung rückgängig, die GETPOST // mit 'restricthtml' auf jedem Klartext anrichtet. Ohne das ginge eine als // Klartext gepflegte Erinnerung als HTML-Mail mit sichtbaren
-Tags raus. $vorlageIshtml = (int) mahnungMailVorlage($stufeObj, $werte)['ishtml']; $bodyRoh = mahnungBodyEntkleiden(GETPOST('message', 'restricthtml'), $vorlageIshtml); $isHtml = dol_textishtml($bodyRoh) ? 1 : 0; $betreff = mahnungHeaderEinzeilig(mahnungErsetzePlatzhalter((string) GETPOST('subject', 'restricthtml'), $werte, 0)); if ($betreff === '') { $sendFehler = $langs->trans('MahnungMailKeinBetreff'); } if ($sendFehler === '') { $mailFehler = ''; $versandOk = mahnungSendeErinnerungsMail( $mahnung, $stufeObj, array( 'to' => implode(', ', $adressen), 'cc' => implode(', ', $adressenCc), 'bcc' => implode(', ', $adressenBcc), 'from' => $absender['voll'], 'subject' => $betreff, 'body' => mahnungErsetzePlatzhalter($bodyRoh, $werte, $isHtml), 'ishtml' => $isHtml, 'paths' => $anhaenge['paths'], 'names' => $anhaenge['names'], 'mimes' => $anhaenge['mimes'], 'deliveryreceipt' => GETPOSTINT('deliveryreceipt'), 'trackid' => 'mah'.((int) $mahnung->id), 'frist' => $neueFrist, // Erneuter Versand eines bereits versendeten Vorgangs (Mail kam nicht // an, ging an die falsche Adresse …). Der Wert kommt aus dem Status, // nicht aus dem Request — der Doppelversand-Schutz greift weiterhin: // die Reservierung nagelt beim erneuten Versand zusätzlich das // bisherige Versanddatum fest, sodass zwei Parallelklicks nicht // beide durchkommen. 'force' => $istErneuterVersand ? 1 : 0, ), $user, $mailFehler ); if ($versandOk) { $formmail->clear_attached_files(); unset($_SESSION[$mahnungInitMarker]); setEventMessages($langs->trans('MahnungErinnerungGesendet'), null, 'mesgs'); header('Location: '.$urlKarte); exit; } $sendFehler = $mailFehler; } } // Fehlgeschlagen: Meldung zeigen und zurück ins Formular, damit die Eingaben // nicht verloren gehen. setEventMessages($sendFehler ?: $langs->trans('MahnungErinnerungSendenFehler'), null, 'errors'); $action = 'presend'; } // --- Anhang vorbelegen ---------------------------------------------------- // Beim Öffnen des Formulars (Button trägt mailinit=1) wird die Anhangsliste // zurückgesetzt und die UNVERÄNDERTE Original-Rechnungs-PDF hineingelegt. // // Die Vorbelegung hängt ausschließlich an `mailinit`, NICHT daran, ob die Liste // leer ist: wer die Rechnung bewusst abwählt, hat danach eine leere Liste — sie // würde sonst im selben Request sofort wieder eingehängt und ließe sich überhaupt // nicht entfernen. Dass schon einmal initialisiert wurde, merkt sich ein // Session-Marker; damit bekommt auch ein Direktaufruf von `action=presend` // (Lesezeichen, Legacy-Redirect) seine Vorbelegung genau einmal. // // Bewusst ein eigener Parameter statt des Dolibarr-üblichen mode=init: FormMail:: // get_form() räumt bei mode=init selbst die Session-Liste leer und würde die hier // eingehängte Rechnung gleich wieder entfernen. // // Der Block läuft vor llxHeader(), damit eine fehlende Rechnungs-PDF sofort als // Warnung erscheint und nicht erst beim nächsten Seitenaufruf. if ($action === 'presend' && $canSendErinnerung && $factureGeladen) { if (GETPOSTINT('mailinit') === 1 || empty($_SESSION[$mahnungInitMarker])) { $formmail->clear_attached_files(); $_SESSION[$mahnungInitMarker] = 1; $pdfFehler = ''; $pdfDatei = mahnungHoleRechnungsPdf($facture, $user, $pdfFehler); if ($pdfDatei !== '') { $formmail->add_attached_files($pdfDatei, basename($pdfDatei), dol_mimetype($pdfDatei)); } elseif ($pdfFehler !== '') { // Ohne Anhang kann trotzdem verschickt werden — der offene Betrag steht im // Mailtext. Deshalb nur eine Warnung, keine Sperre. setEventMessages($pdfFehler, null, 'warnings'); } } } // Upload-Verzeichnis für Sendebelege (muss VOR llxHeader stehen für 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, mahnungSetupLink(), 'fa-envelope-open-text'); print '
'; print '
'; print ''; print ''; // Stufe als EIN Badge (Nummer + Bezeichnung). Vorher stand die Bezeichnung als // Text da und daneben noch ein Badge mit demselben Wort. print ''; print ''; print ''; print ''; // Rechnung if ($factureGeladen) { print ''; print ''; } // Kunde if ($societeGeladen) { print ''; print ''; } print ''; if ($istErinnerung) { // Kostenlose Zahlungserinnerung: keine Gebühren-, Pauschalen- oder Zinszeilen // anzeigen — es fällt bewusst nichts an. Ein Altvorgang mit gespeicherten Beträgen // behält seine Kostenzeilen (siehe $hatGespeicherteKosten weiter oben), auch wenn // die Stufe inzwischen als Erinnerung markiert wurde. print ''; } else { print ''; if ((float) $mahnung->pauschale_b2b > 0) { print ''; } if ((float) $mahnung->kosten_vorstufen > 0) { print ''; } // Ausgewiesener Zinssatz exakt wie im Schreiben (siehe // pdf_standard_mahnung::getAusgewiesenerZinssatz()): der Stufen-Override hat // Vorrang, nur ohne Override gilt Basiszins-Snapshot + Aufschlag. Vorher stand // hier bloß der Basiszins — bei gesetztem Override wich die Karte damit vom // tatsächlich berechneten und im PDF ausgewiesenen Satz ab. $zinsOverride = $stufeGeladen ? $stufeObj->getZinssatzOverride($mahnung->customertype) : null; $basisSnapshot = ($mahnung->basiszins_snapshot !== null) ? (float) $mahnung->basiszins_snapshot : 0.0; if ($zinsOverride !== null) { $zinssatzAusgewiesen = (float) $zinsOverride; $zinsHerkunft = $langs->trans('MahnungZinssatzOverrideStufe'); } else { $zinsAufschlag = ($mahnung->customertype === Mahnung::KUNDENTYP_B2B) ? (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2B', '9.0') : (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2C', '5.0'); $zinssatzAusgewiesen = $basisSnapshot + $zinsAufschlag; $zinsHerkunft = $langs->trans('MahnungZinssatzAusBasiszins', number_format($basisSnapshot, 2, ',', '.')); } // $langs->trans() kodiert sein Ergebnis selbst per htmlentities — kein zweites // Escaping, das würde Sonderzeichen doppelt kodieren. print ''; print ''; } print ''; print '
'.$langs->trans('MahnungRef').''.dol_escape_htmltag($mahnung->ref).'
'.$langs->trans('MahnungStufe').''; print mahnungStufeBadge((int) $mahnung->stufe, $stufeGeladen ? $stufeObj->label : '', $istErinnerung); print '
'.$langs->trans('MahnungDatum').''.dol_print_date($mahnung->date_mahnung, 'day').'
'.$langs->trans('MahnungFaelligkeitAlt').''.dol_print_date($mahnung->date_lim_reglement_alt, 'day').'
'.$langs->trans('MahnungFaelligkeitNeu').''.dol_print_date($mahnung->date_lim_reglement_neu, 'day').'
'.$langs->trans('MahnungRechnung').''.dol_escape_htmltag($facture->ref).'
'.$langs->trans('MahnungKunde').''.dol_escape_htmltag($societe->name).' ('.dol_escape_htmltag((string) $mahnung->customertype).')
'.$langs->trans('MahnungBetragOffen').''.price($mahnung->betrag_offen).'
'.$langs->trans('MahnungKosten').''.dol_escape_htmltag($langs->trans('MahnungErinnerungKostenfrei')).'
'.$langs->trans('MahnungGebuehr').''.price($mahnung->mahngebuehr).'
'.$langs->trans('MahnungPauschaleB2B').''.price($mahnung->pauschale_b2b).'
'.$langs->trans('MahnungKostenVorstufen').''.price($mahnung->kosten_vorstufen).'
'.$langs->trans('MahnungVerzugszinsen').''.price($mahnung->verzugszinsen) .' ('.number_format($zinssatzAusgewiesen, 2, ',', '.').' % p.a. — '.$zinsHerkunft.')
'.$langs->trans('MahnungSumme').''.price($mahnung->summe_mahnung).'
'.$langs->trans('Status').''.dol_escape_htmltag($mahnung->getStatusLabel()).'
'; print '
'; // Ende fichecenter Stammdaten // --- Generierte Dokumente (wie bei Rechnungen) --- print '
'; print load_fiche_titre($langs->trans('Documents'), '', 'fa-file'); if ($istErinnerung) { // Für eine Zahlungserinnerung wird bewusst KEIN Mahn-PDF erzeugt. Angehängt // wird die unveränderte Original-Rechnung. // Kein dol_escape_htmltag drumherum: $langs->trans() setzt den Parameter per // sprintf ein und kodiert das Ergebnis danach selbst per htmlentities. // Ein zweites Escaping würde Sonderzeichen im Dateinamen doppelt kodieren. print '
'; print $langs->trans('MahnungErinnerungAnhangHinweis', $anhangDatei !== '' ? $anhangDatei : '-'); print '
'; } // Dokumente im Rechnungsordner suchen die zur Mahnung gehoeren $docDir = ''; if ($facture->id > 0) { $docDir = !empty($conf->facture->multidir_output[$facture->entity]) ? $conf->facture->multidir_output[$facture->entity] : $conf->facture->dir_output; $docDir .= '/'.dol_sanitizeFileName($facture->ref); } $fileList = array(); if (!empty($docDir) && is_dir($docDir)) { $allFiles = dol_dir_list($docDir, 'files', 0, preg_quote($mahnungSafeRef, '/')); foreach ($allFiles as $f) { $fileList[] = $f; } } // Bei einer Zahlungserinnerung gehört die Original-Rechnung mit in die Liste: sie ist // das Dokument, das tatsächlich rausgeht. Ohne sie stünde hier "keine Dokumente", und // zum Nachschauen müsste man erst auf die Rechnungskarte wechseln. // // Bewusst mahnungFindeRechnungsPdf() statt mahnungHoleRechnungsPdf(): das bloße // Anschauen der Karte darf keine PDF-Erzeugung auslösen. Fehlt die Datei noch, wird // sie beim Öffnen des Mailformulars erzeugt. $rechnungsPdf = null; if ($istErinnerung && $factureGeladen) { $rechnungsPdfPfad = mahnungFindeRechnungsPdf($facture); if ($rechnungsPdfPfad !== '') { $rechnungsPdf = array( 'name' => basename($rechnungsPdfPfad), 'fullname' => $rechnungsPdfPfad, 'relativ' => dol_sanitizeFileName($facture->ref).'/'.basename($rechnungsPdfPfad), ); } } if (!empty($fileList) || $rechnungsPdf !== null) { $canDeleteDoc = $user->hasRight('mahnung', 'write'); // Eigener Lang-Key statt trans('Document'): den Singular gibt es in Dolibarrs // deutschem Sprachpaket nicht, dort stand deshalb "Document" auf Englisch. // Vorschau bekommt eine eigene, schmale Spalte — direkt neben dem Dateinamen // klebte die Lupe am Text. print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; foreach ($fileList as $f) { $fname = $f['name']; $relativePath = dol_sanitizeFileName($facture->ref).'/'.$fname; $dlUrl = DOL_URL_ROOT.'/document.php?modulepart=facture&file='.urlencode($relativePath); $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 print ''; // Vorschau (nur PDF) print ''; // Größe — mit shortvalue/shortunit, sonst stünde dort die rohe Byte-Zahl print ''; // Datum print ''; // Aktionen: Download + Löschen print ''; print ''; } // Original-Rechnung der Zahlungserinnerung — gleiche Darstellung wie oben, aber // ohne Löschen-Knopf: die Datei gehört der Rechnung, nicht dem Mahnvorgang. if ($rechnungsPdf !== null) { $rDlUrl = DOL_URL_ROOT.'/document.php?modulepart=facture&file='.urlencode($rechnungsPdf['relativ']); $rViewUrl = $rDlUrl.'&attachment=0'; $rSize = (int) @filesize($rechnungsPdf['fullname']); $rDate = (int) @filemtime($rechnungsPdf['fullname']); print ''; print ''; print ''; print ''; print ''; print ''; print ''; } print '
'.$langs->trans('MahnungDokument').''.$langs->trans('MahnungVorschau').''.$langs->trans('Size').''.$langs->trans('Date').'
'; print ''; print img_picto('', $icon, 'class="pictofixedwidth"'); print dol_escape_htmltag($fname); print ''; print ''; if ($ext === 'pdf') { print ''.img_picto($langs->trans('Preview'), 'search').''; } print ''.dol_print_size($filesize, 1, 1).''.dol_print_date($filedate, 'dayhour').''; 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 '
'; print ''; print img_picto('', 'pdf', 'class="pictofixedwidth"'); print dol_escape_htmltag($rechnungsPdf['name']); print ''; // Als Badge statt als grauer Fließtext — sonst verschwimmt der Hinweis // mit dem Dateinamen. print ' '; print dol_escape_htmltag($langs->trans('MahnungErinnerungAnhangOriginal')); print ''; print ''; print ''.img_picto($langs->trans('Preview'), 'search').''; print ''.($rSize > 0 ? dol_print_size($rSize, 1, 1) : '').''.($rDate > 0 ? dol_print_date($rDate, 'dayhour') : '').''; print ''.img_picto($langs->trans('Download'), 'download').''; print '
'; } elseif ($istErinnerung) { // Erinnerung ohne auffindbare Rechnungs-PDF: sie wird beim Öffnen des // Mailformulars erzeugt, hier gibt es deshalb noch nichts zu zeigen. print '
'.$langs->trans('MahnungErinnerungRechnungsPdfNochNicht').'
'; } else { print '
'.$langs->trans('NoDocuments').'
'; } // Aktionen require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php'; $form = new Form($db); $formfile = new FormFile($db); // Postalischer Versand (Versandweg, Sendungsverfolgung, Belege) ist nur sinnvoll, // wenn die Mahnung tatsächlich auf dem Postweg rausgeht. Eine Zahlungserinnerung — // und generell jeder Vorgang mit Versandart "E-Mail" — braucht weder Einschreiben- // Nummer noch Belegscan. Statt der kompletten Sektion wird dann nur der // Mail-Versandstatus angezeigt. $istMailVersand = ($mahnung->versandart === Mahnung::VERSAND_MAIL) || $istErinnerung; if ($istMailVersand) { print '
'; print load_fiche_titre($langs->trans('MahnungVersandStatus'), '', 'fa-envelope'); print '
'; print '
'; print ''; print ''; print ''; print ''; print '
'.$langs->trans('MahnungVersandart').''; print dol_escape_htmltag($langs->trans('MahnungVersandwegEmail')); print '
'.$langs->trans('MahnungEmpfaenger').''; $empfaenger = ''; if (!empty($mahnung->fk_soc)) { $socMail = new Societe($db); if ($socMail->fetch((int) $mahnung->fk_soc) > 0) { $empfaenger = trim((string) $socMail->email); } } if ($empfaenger !== '') { print dol_escape_htmltag($empfaenger); } else { print ''.dol_escape_htmltag($langs->trans('MahnungKundeKeineEmail')).''; } print '
'.$langs->trans('MahnungDateVersand').''; if (!empty($mahnung->date_versand)) { print dol_print_date($mahnung->date_versand, 'dayhour'); } else { print ''.dol_escape_htmltag($langs->trans('MahnungNochNichtVersendet')).''; } print '
'; // --- Versandprotokoll --------------------------------------------------- // Was ist wann an wen rausgegangen? Festgehalten wird der Stand zum // Sendezeitpunkt; eine später geänderte Stufen-Vorlage ändert das Protokoll // nicht. Mehrere Einträge entstehen beim erneuten Versand. $mailProtokoll = mahnungHoleMailProtokoll((int) $mahnung->id); if (!empty($mailProtokoll)) { print '
'; print ''; print ''; print ''; print ''; foreach ($mailProtokoll as $eintrag) { print ''; } print '
'.$langs->trans('MahnungMailProtokoll').'
'; // Kopfzeile: zugeklappt sieht man Zeitpunkt und Empfänger, mehr braucht // es im Regelfall nicht. print '
'; print ''; print ''.dol_print_date($eintrag->date_versand, 'dayhour').''; print '   '.dol_escape_htmltag($eintrag->mail_to); print ''; print '
'; print ''; print ''; print ''; if (trim((string) $eintrag->mail_cc) !== '') { print ''; } if (trim((string) $eintrag->mail_bcc) !== '') { print ''; } print ''; if (trim((string) $eintrag->anhaenge) !== '') { print ''; } print '
'.$langs->trans('MahnungMailVon').''.dol_escape_htmltag((string) $eintrag->mail_from).'
'.$langs->trans('MahnungMailAn').''.dol_escape_htmltag((string) $eintrag->mail_to).'
'.$langs->trans('MahnungMailKopie').''.dol_escape_htmltag((string) $eintrag->mail_cc).'
'.$langs->trans('MahnungMailBlindkopie').''.dol_escape_htmltag((string) $eintrag->mail_bcc).'
'.$langs->trans('MahnungMailBetreff').''.dol_escape_htmltag((string) $eintrag->subject).'
'.$langs->trans('MahnungMailAnhaenge').''.dol_escape_htmltag((string) $eintrag->anhaenge).'
'; // Der Text so, wie er verschickt wurde. Bei HTML durch // dol_string_onlythesehtmltags() — der Inhalt kam zwar schon gefiltert // herein, aber ausgegeben wird er hier ungefragt im Browser. print '
'; if (!empty($eintrag->ishtml)) { print dol_string_onlythesehtmltags((string) $eintrag->body); } else { print nl2br(dol_escape_htmltag((string) $eintrag->body)); } print '
'; print '
'; print '
'; print '
'; } print '
'; } else { // --- Versand & Belege --- print '
'; print load_fiche_titre($langs->trans('MahnungVersandBelege'), '', 'fa-truck'); print '
'; print '
'; // Versandwege (Dropdown-Optionen, Label kommt aus Lang-File MahnungVersandweg*) $versandwege = array( 'post' => $langs->trans('MahnungVersandwegPost'), 'einschreiben' => $langs->trans('MahnungVersandwegEinschreiben'), 'dhl' => $langs->trans('MahnungVersandwegDhl'), 'dpd' => $langs->trans('MahnungVersandwegDpd'), 'hermes' => $langs->trans('MahnungVersandwegHermes'), 'ups' => $langs->trans('MahnungVersandwegUps'), 'fax' => $langs->trans('MahnungVersandwegFax'), 'email' => $langs->trans('MahnungVersandwegEmail'), 'persoenlich' => $langs->trans('MahnungVersandwegPersoenlich'), 'eigen' => $langs->trans('MahnungVersandwegEigen'), ); // $canWrite wird bereits oben (vor den Action-Handlern) gesetzt $editVersand = ($action === 'edit_versand') || empty($mahnung->date_versand); if (!empty($mahnung->date_versand) && $action !== 'edit_versand') { // Anzeige der bereits erfassten Versanddaten print ''; print ''; print ''; if (!empty($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 ''; } print '
'.$langs->trans('MahnungVersanddatum').''.dol_print_date($mahnung->date_versand, 'day').'
'.$langs->trans('MahnungVersandweg').'' .($mahnung->versandweg && isset($versandwege[$mahnung->versandweg]) ? dol_escape_htmltag($versandwege[$mahnung->versandweg]) : dol_escape_htmltag((string) $mahnung->versandweg)) .'
'.$langs->trans('MahnungTrackingNr').''; print ''.dol_escape_htmltag($mahnung->tracking_nr).''; if (!empty($trackUrl)) { print ' '; print img_picto('', 'fa-external-link-alt', 'class="pictofixedwidth"'); print dol_escape_htmltag($langs->trans('MahnungSendungVerfolgen')).''; } print '
'; if ($canWrite) { print '
'; print ''.img_picto('', 'edit').' '.dol_escape_htmltag($langs->trans('MahnungVersandBearbeiten')).' '; print ''.dol_escape_htmltag($langs->trans('MahnungVersandLeeren')).''; print '
'; } } elseif ($canWrite) { // Versand-Formular (Erfassung oder Bearbeitung) $dateInit = !empty($mahnung->date_versand) ? $mahnung->date_versand : dol_now(); print '
'; print ''; print ''; print ''; // Versanddatum print ''; // Versandweg print ''; // Tracking-Nr print ''; // Optional: Tracking-Provider override print ''; print '
'.$langs->trans('MahnungVersanddatum').''; print $form->selectDate($dateInit, 'versand_', 0, 0, 0, '', 1, 0); print '
'.$langs->trans('MahnungVersandweg').''; print ''; print '
'.$langs->trans('MahnungTrackingNr').''; print ''; print ' ('.dol_escape_htmltag($langs->trans('MahnungTrackingProviderAuto')).')'; print '
'.$langs->trans('MahnungTrackingProvider').''; print ''; print '
'; print '
'; print ' '; if (!empty($mahnung->date_versand)) { print ''.dol_escape_htmltag($langs->trans('Cancel')).''; } print '
'; print '
'; } // --- Sendebelege (Beleg-Upload via Dolibarr-Standard) --- print '
'; print load_fiche_titre($langs->trans('MahnungSendebelege'), '', 'fa-paperclip'); print '
'.$langs->trans('MahnungSendebelegeHint').'
'; // Tracking-Vorschläge aus Session-Flash (vom Scan) anzeigen $suggKey = 'mahnung_tracking_suggestions_'.((int) $mahnung->id); $suggestions = $_SESSION[$suggKey] ?? null; if (is_array($suggestions) && !empty($suggestions)) { print '
'; print ''.$langs->trans('MahnungTrackingErkannt').' ('.count($suggestions).')'; print ''; foreach ($suggestions as $sg) { print ''; print ''; print ''; print ''; print ''; print ''; print ''; } print '
'.img_picto('', 'pdf', 'class="pictofixedwidth"').dol_escape_htmltag($sg['file']).''.dol_escape_htmltag($sg['label']).''.dol_escape_htmltag($sg['nr']).''.img_picto('', 'fa-external-link-alt').''; if ($canWrite) { $applyUrl = $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id).'&action=apply_tracking' .'&nr='.urlencode((string) $sg['nr']) .'&provider='.urlencode((string) $sg['provider']) .'&token='.newToken(); print ''.dol_escape_htmltag($langs->trans('MahnungTrackingUebernehmen')).''; } print '
'; if ($canWrite) { print '
'.dol_escape_htmltag($langs->trans('MahnungTrackingVerwerfen')).'
'; } print '
'; } // Scan-Button (Belege durchsuchen) if ($canWrite) { print '
'; print ''; print img_picto('', 'fa-search', 'class="pictofixedwidth"').dol_escape_htmltag($langs->trans('MahnungBelegeScannen')); print ' '; print ''.$langs->trans('MahnungBelegeScannenHint').''; print '
'; } $urlSelf = $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id); // Upload-Formular (Durchsuchen + Upload-Button) $formfile->form_attach_new_file( $urlSelf, '', // title 0, // addcancel 0, // sectionid (int) $canWrite, // perm 50, // size $mahnung, // object '', // options 1, // useajax '', // savingdocmask 0, // linkfiles 'formuserfile', // htmlname '', // accept '', // sectiondir 0, // usewithoutform 0, // capture 0 // disablemulti ); // Dateiliste der bereits hochgeladenen Belege print $formfile->showdocuments( 'mahnung', // $modulepart $mahnungSafeRef, // $modulesubdir $upload_dir, // $filedir $urlSelf, // $urlsource 0, // $genallowed (kein PDF-Gen-Button hier) (int) $canWrite, // $delallowed '', // $modelselected 0, // $allowgenifempty 0, // $forcenomultilang 0, // $iconPDF 0, // $notused 0, // $noform '', // $param '', // $title '', // $buttonlabel '', // $codelang '', // $morepicto $mahnung, // $object 0 // $hideifempty ); print '
'; // Ende fichecenter Versand & Belege } if ($mahnung->status !== Mahnung::STATUS_STORNIERT && $user->hasRight('mahnung', 'delete')) { if ($action === 'confirm_storno') { print $form->formconfirm( $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id), $langs->trans('MahnungStornieren'), $langs->trans('MahnungStornieren').' — '.dol_escape_htmltag($mahnung->ref).'?', 'storno', '', 0, 1 ); } if ($action === 'ask_uneinbringlich') { // Bestätigungsdialog mit Eingabefeld für Begründung $formquestion = array( array( 'type' => 'textarea', 'name' => 'uneinbringlich_note', 'label' => $langs->trans('MahnungUneinbringlichBegruendung'), 'value' => $langs->trans('MahnungVerfahrenErfolglos').' '.dol_print_date(dol_now(), 'day'), 'morecss' => 'minwidth500 quatrevingtpercent', ), ); print $form->formconfirm( $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id), $langs->trans('MahnungUneinbringlichTitel'), $langs->trans('MahnungUneinbringlichWarnung').' '.dol_escape_htmltag($mahnung->ref).'?', 'confirm_uneinbringlich', $formquestion, 0, 1, 300 ); } } print '
'; // Mahn-PDF: für Zahlungserinnerungen gibt es keins — dort wird die unveränderte // Original-Rechnung angehängt, kein eigenes Dokument erzeugt. if ($canWrite && !$istErinnerung) { // Modellauswahl require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/mahnung/modules_mahnung.php'; $modellist = ModelePDFMahnung::liste_modeles($db); $defaultModel = getDolGlobalString('MAHNUNG_ADDON_PDF', 'standard_mahnung'); if (is_array($modellist) && count($modellist) > 1) { print ''; } print ''; print $langs->trans('MahnungGenerate'); print ' '; } // "Zahlungserinnerung per E-Mail senden" — ausschließlich für Erinnerungs-Stufen. // Echte Mahnungen (Stufe ohne ist_erinnerung) werden bewusst NICHT per Mail // verschickt, weil der Zugang dort nicht beweisbar ist. // mailinit=1 setzt die Anhangsliste zurück und legt die Original-Rechnung frisch // hinein — sonst hingen bei jedem erneuten Öffnen die alten Dateien noch dran. if ($canSendErinnerung) { // Nach einem bereits erfolgten Versand heißt der Button "erneut senden": die Mail // kann unzustellbar zurückgekommen sein, im Spam hängen oder an die falsche // Adresse gegangen sein. Der Empfänger lässt sich im Formular ändern. $sendeLabel = $istErneuterVersand ? 'MahnungErinnerungErneutSenden' : 'MahnungErinnerungSenden'; $sendeHint = $istErneuterVersand ? 'MahnungErinnerungErneutSendenHint' : 'MahnungErinnerungSendenHint'; print ''; print img_picto('', 'email', 'class="pictofixedwidth"'); print $langs->trans($sendeLabel); print ' '; } elseif ($istErinnerung && (int) $mahnung->status !== Mahnung::STATUS_ERLEDIGT && (int) $mahnung->status !== Mahnung::STATUS_STORNIERT && $user->hasRight('mahnung', 'send') && !$erinnerungEmpfaengerMoeglich ) { // Grund für den deaktivierten Button sichtbar machen print ''; print $langs->trans('MahnungErinnerungSenden'); print ' '; } // "Als uneinbringlich klassifizieren" — der letzte Schritt im Eskalationspfad. // Angeboten wird er, wenn nach der Stufe dieses Vorgangs keine weitere aktive Stufe // mehr konfiguriert ist ($istLetzteStufe, siehe MahnungVorschlag::naechsteStufeNach()), // der Vorgang keine kostenlose Zahlungserinnerung ist, Status >= ERSTELLT gilt (also // kein Entwurf) und nichts storniert wurde. Früher hing das an der harten Grenze // stufe === 3 — mit frei konfigurierbaren Stufen war der Button damit bei jeder // abweichenden Stufennummer unerreichbar. // Setzt die Rechnung auf STATUS_ABANDONED mit close_code='badcustomer'. if ($mahnung->status !== Mahnung::STATUS_STORNIERT && $istLetzteStufe && !$istErinnerung && (int) $mahnung->status >= Mahnung::STATUS_ERSTELLT && $user->hasRight('mahnung', 'delete') // Der Schritt schreibt auf der Rechnung — ohne Rechnungsrecht würde der Button // nur in eine Fehlermeldung führen (siehe Handler oben). && $user->hasRight('facture', 'creer') ) { // Prüfen ob Rechnung schon abandoned ist — dann Button verstecken $facStatus = 0; $facRes = $db->query("SELECT fk_statut, close_code FROM ".MAIN_DB_PREFIX."facture WHERE rowid = ".((int) $mahnung->fk_facture)); if ($facRes && ($facObj = $db->fetch_object($facRes))) { $facStatus = (int) $facObj->fk_statut; $db->free($facRes); } if ($facStatus !== 3) { print ''; print img_picto('', 'warning', 'class="pictofixedwidth"'); print $langs->trans('MahnungUneinbringlich'); print ''; } else { // Rechnung ist schon als abandoned markiert — Info anzeigen print ''.dol_escape_htmltag($langs->trans('MahnungRechnungBereitsUneinbringlich')).''; } } if ($mahnung->status !== Mahnung::STATUS_STORNIERT && $user->hasRight('mahnung', 'delete')) { print ''; print $langs->trans('MahnungStornieren'); print ''; } print '
'; // --------------------------------------------------------------------------- // Mailformular für die Zahlungserinnerung // --------------------------------------------------------------------------- // Standard-Dolibarr-Formular: Empfänger (Firmenadresse + alle Ansprechpartner), // Betreff, Text und Anhang stehen sichtbar da und sind änderbar. Abgeschickt wird // gegen action=send auf dieser Karte — der Versand selbst läuft ausschließlich über // mahnungSendeErinnerungsMail() (siehe oben). if ($action === 'presend' && $canSendErinnerung) { print '
'; print load_fiche_titre($langs->trans($istErneuterVersand ? 'MahnungErinnerungErneutSenden' : 'MahnungErinnerungSenden'), '', 'fa-envelope'); if ($istErneuterVersand) { // Deutlich sichtbar, dass diese Erinnerung schon einmal raus war — sonst // verschickt man sie versehentlich ein zweites Mal an denselben Empfänger. print '
'; print $langs->trans( 'MahnungErinnerungErneutSendenWarnung', $mahnung->date_versand ? dol_print_date($mahnung->date_versand, 'dayhour') : '-' ); print '
'; } print '
'.$langs->trans('MahnungErinnerungMailHint').'
'; // Betreff und Text aus der Stufen-Konfiguration, Platzhalter bereits ersetzt. // Die Frist wird zum jetzigen Zeitpunkt gerechnet; beim Absenden passiert das // erneut, damit eine später abgeschickte Mail keine verstrichene Frist nennt. $mailFrist = mahnungBerechneNeueFrist($stufeObj, dol_now()); $mailWerte = mahnungPlatzhalterWerte($mahnung, $facture, $societe, $mailFrist, $stufeObj); $mailVorlage = mahnungMailVorlage($stufeObj, $mailWerte); // Absender ist die Firmenadresse, nicht der angemeldete Benutzer — deshalb nur // anzeigen (withfromreadonly), nicht zur Auswahl stellen. $absenderAnzeigeFehler = ''; $absenderAnzeige = mahnungAbsender($absenderAnzeigeFehler); if ($absenderAnzeigeFehler !== '') { print '
'.dol_escape_htmltag($absenderAnzeigeFehler).'
'; } // param-Einträge werden von FormMail als hidden inputs ausgegeben und landen // damit im POST des Formulars. $formmail->param['action'] = 'send'; $formmail->param['id'] = (int) $mahnung->id; $formmail->param['returnurl'] = $urlKarte; // 'none' = keine Vorlagen aus c_email_templates anbieten. Betreff und Text // kommen aus der Stufen-Konfiguration des Moduls; eine zweite Textquelle // daneben würde nur auseinanderlaufen. $formmail->param['models'] = 'none'; $formmail->withform = 1; $formmail->fromtype = 'special'; $formmail->fromname = $absenderAnzeige['name']; $formmail->frommail = $absenderAnzeige['mail']; $formmail->withfrom = 1; $formmail->withfromreadonly = 1; // Auswahlliste: Firmenadresse + alle aktiven Ansprechpartner des Kunden. $formmail->withto = mahnungEmpfaengerListe($societe); // Nach einem fehlgeschlagenen Versand den eingetippten Freitext-Empfänger // zurückschreiben. FormMail füllt das Feld sonst nur aus GETPOST('sendto'), wenn // withtofree numerisch ist — bei einem nicht-numerischen Wert gilt dieser als // Feldinhalt, genau das brauchen wir hier. $formmail->withtofree = GETPOSTISSET('sendto') ? GETPOST('sendto', 'alphawithlgt') : 1; $formmail->withtocc = 1; $formmail->withtopic = $mailVorlage['subject']; // Eine bereits getippte Nutzereingabe hat Vorrang vor der Vorlage — sie kommt // aber
-verseucht aus GETPOST(…, 'restricthtml') zurück (siehe // mahnungBodyEntkleiden). Deshalb: bereinigen, als Vorgabewert setzen und den // Rohwert aus dem Request nehmen, sonst zeigt FormMail wieder die Tag-Fassung. $formmail->withbody = $mailVorlage['body']; if (GETPOSTISSET('message')) { $formmail->withbody = mahnungBodyEntkleiden(GETPOST('message', 'restricthtml'), (int) $mailVorlage['ishtml']); unset($_POST['message'], $_GET['message']); } // -1 = Dolibarr entscheidet anhand von FCKEDITOR_ENABLE_MAIL, genau wie beim // Mailversand aus Rechnungen und Aufträgen. Damit lässt sich der Text hier mit // dem gewohnten Editor formatieren (fett, Listen, Links); ist der WYSIWYG global // abgeschaltet, bleibt es ein einfaches Textfeld. $formmail->withfckeditor = -1; // 2 = Anhänge anzeigen UND weitere hinzufügen/entfernen können. $formmail->withfile = 2; $formmail->withdeliveryreceipt = 1; $formmail->withcancel = 1; print $formmail->get_form(); } llxFooter(); $db->close();