mahnung/admin/setup.php
Eduard Wisch 0244a5b07e feat(mahnung): Mailversand ueber FormMail, Versandprotokoll, HTML-Mails
Phase 13/14 abgeschlossen. Der Mailversand war zuvor KAPUTT: ajax/sendmail.php
war bereits zur Funktionsbibliothek umgebaut, card.php postete aber weiterhin
per JS dagegen.

Mailformular (FormMail)
- card.php nutzt jetzt Dolibarrs Standard-Mailformular (action=presend/send):
  Empfaenger (Firma + alle Ansprechpartner), Betreff, Text und Anhang sichtbar
  und aenderbar. Gesendet wird ausschliesslich ueber mahnungSendeErinnerungsMail().
- Anhang = unveraenderte Original-Rechnungs-PDF, wird bei Bedarf nacherzeugt.
  Eigener Parameter mailinit statt mode=init, weil get_form() bei mode=init die
  Anhangsliste selbst leert.
- HTML-Mails: DolEditor im Setup + withfckeditor=-1 im Formular (folgt
  FCKEDITOR_ENABLE_MAIL wie Dolibarrs eigene Mailvorlagen).
- Klartext bleibt Klartext: GETPOST('restricthtml') jagt jeden Nicht-HTML-Text
  durch dol_nl2br() — mahnungBodyEntkleiden() nimmt nur dieses Artefakt zurueck
  und laesst echte Formatierung unangetastet.
- Platzhalter jetzt auch in Dolibarr-Schreibweise (__REF__, __DATE_YMD__,
  __AMOUNT_FORMATED__, __DATE_DUE_YMD__, __FRIST_TAGE__ ...), Liste sichtbar im Setup.
- Absender-Adresse und -Name konfigurierbar (MAHNUNG_EMAIL_SENDER[_NAME]).
- Erneuter Versand moeglich (force aus dem Status abgeleitet, nicht aus dem
  Request — Doppelversand-Schutz bleibt wirksam).

Versandprotokoll (neue Tabelle llx_mahnung_mailprotokoll)
- Jede versendete Erinnerung wird mit Empfaenger, Kopie, Betreff, Text und
  Anhangsnamen festgehalten, einsehbar unter Versandstatus. Historie statt
  Spalten am Vorgang, weil erneut gesendet werden kann.
- Lazy-Migration legt die Tabelle an (DB_VERSION 0.4.0), kein Reaktivieren noetig.

Haertung nach Code-Review (21 bestaetigte Funde)
- Anhang liess sich nicht abwaehlen (wurde sofort wieder eingehaengt)
- Upload/Entfernen ohne Rechtepruefung; Temp-Verzeichnis pro Vorgang getrennt
- Teilzahlung zwischen Oeffnen und Senden fuehrt zurueck ins Formular
- Empfaenger: Semikolon-Trenner, keine stillen Verwerfungen, Dubletten, CR/LF
- CSRF: presend + Core-Dateiaktionen (confirm_deletefile, renamefile, sendit,
  linkit) token-pflichtig
- Externe Benutzer sehen nur eigene Vorgaenge; Abschreiben verlangt facture.creer

UI
- Mahnstufe nur noch EINE Darstellung (Badge), Farbskala zentral in
  lib/mahnung_ui.lib.php statt doppelt gepflegt
- Zahnrad oben rechts in die Einstellungen (nur mit Recht mahnung.setup)
- Original-Rechnung unter Verknuepfte Dokumente mit Vorschau, Groesse in KB
- PDF-Einleitungstext nur noch, wo ueberhaupt ein PDF entsteht

Sprachdateien de_DE/en_US deckungsgleich, 9 tote Keys entfernt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:31:25 +02:00

1467 lines
63 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* 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/admin/setup.php
* \ingroup mahnung
* \brief Setup: Mahnstufen (frei anlegbar, bearbeitbar und löschbar), Basiszins,
* B2C/B2B-Aufschläge, Pauschale, Ntfy-Topic.
*/
$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 && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
}
if (!$res && file_exists("../../main.inc.php")) {
$res = @include "../../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.'/core/lib/admin.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/doleditor.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/mahnung/modules_mahnung.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/modMahnung.class.php';
global $langs, $user, $conf, $db;
$langs->loadLangs(array('admin', 'mahnung@mahnung'));
if (!$user->admin && !$user->hasRight('mahnung', 'setup')) {
accessforbidden();
}
// Schema-Migration bei jedem Setup-Aufruf (idempotent — fehlende Spalten ergänzen)
(new modMahnung($db))->migrateVersandFelder();
// Tracking-Pattern-Tabelle anlegen (falls noch nicht da) + Default-Patterns seeden
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungtrackingpattern.class.php';
$check = $db->query("SHOW TABLES LIKE '".$db->escape(MAIN_DB_PREFIX.'mahnung_trackingpattern')."'");
if ($check && $db->num_rows($check) == 0) {
$db->query("CREATE TABLE ".MAIN_DB_PREFIX."mahnung_trackingpattern ("
." rowid INTEGER AUTO_INCREMENT PRIMARY KEY,"
." entity INTEGER DEFAULT 1 NOT NULL,"
." provider VARCHAR(20) NOT NULL,"
." label VARCHAR(80) NOT NULL,"
." regex VARCHAR(255) NOT NULL,"
." url_template VARCHAR(255) NOT NULL,"
." priority INTEGER DEFAULT 100,"
." active TINYINT DEFAULT 1,"
." datec DATETIME,"
." tms TIMESTAMP,"
." INDEX idx_trackingpattern_active (active, priority),"
." INDEX idx_trackingpattern_entity (entity)"
.") ENGINE=InnoDB");
}
if ($check) {
$db->free($check);
}
MahnungTrackingPattern::seedDefaults($db);
$form = new Form($db);
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
$rowid = GETPOSTINT('rowid');
// --- 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 die Konstante
// MAIN_SECURITY_CSRF_WITH_TOKEN=0. Der komplette Prüfblock ab main.inc.php:333
// wird damit übersprungen — die Formulare schicken zwar newToken() mit, es hat
// bisher nur nie jemand verglichen. Ohne eigene Prüfung reicht deshalb ein
// <img src=".../custom/mahnung/admin/setup.php?action=confirm_delete_stufe&confirm=yes&rowid=5">
// auf einer beliebigen fremden Seite, um im eingeloggten Kontext still eine
// Mahnstufe zu löschen. Die Löschsperre weiter unten hilft dagegen nicht: sie
// greift nur, wenn bereits Mahnvorgänge auf die Stufennummer verweisen — die
// frisch geseedete Zahlungserinnerung (Stufe 0) ist genau der ungeschützte Fall.
//
// 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 steckt das mitgeschickte Token in 'token', während 'newtoken' schon
// neu gewürfelt ist — eine Prüfung nur gegen 'newtoken' würde dann jede Aktion
// dieser Seite blockieren. hash_equals() statt ===: konstante Laufzeit.
$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)));
}
// Alle Actions mit Seiteneffekt. 'ask_delete_stufe' steht bewusst NICHT drin:
// die rendert nur den Bestätigungsdialog und ändert nichts in der DB.
// Neue schreibende Actions gehören hier zwingend mit rein.
// 'deletefile' wird weiter unten von actions_setmoduleoptions.inc.php abgearbeitet
// (löscht ein hochgeladenes ODT-Template) und ist genauso ungeschützt — der Link
// im ODT-Modell hängt sein Token an, geprüft hat es bisher niemand.
$actionsMitToken = array(
'save_consts',
'save_stufen',
'add_stufe',
'confirm_delete_stufe',
'setdoc',
'set',
'del',
'deletefile',
);
// Zusätzlich echtes POST erzwingen — aber nur dort, wo das Formular auch per
// POST abschickt. NICHT bei confirm_delete_stufe: $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 das Löschen komplett lahmlegen. Ebenso ausgenommen
// setdoc/set/del/deletefile — das sind Links in der Dokumentenmodell-Tabelle.
// Auf diesen Wegen wird das Token trotzdem mitgeschickt (in der URL) und für
// sich genommen reicht es: erraten kann ein Angreifer es nicht.
$actionsNurPost = array('save_consts', 'save_stufen', 'add_stufe');
if (in_array($action, $actionsMitToken, true)) {
$csrfFehler = '';
if (!$tokenOk) {
$csrfFehler = $langs->trans('MahnungCsrfTokenUngueltig');
} elseif (in_array($action, $actionsNurPost, true)
&& (empty($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'POST')) {
$csrfFehler = $langs->trans('MahnungNurPostErlaubt');
}
if ($csrfFehler !== '') {
setEventMessages($csrfFehler, null, 'errors');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
}
// Nach fehlgeschlagener Validierung: die eingegebenen Werte bleiben im Formular stehen
$stufenPost = array(); // rowid => MahnungStufe mit den POST-Werten
$neuStufePost = null; // Vorbelegung des Anlege-Formulars
/**
* Auswahlliste der Versandarten: DB-Wert => Lang-Key.
*
* @return array
*/
function mahnungSetupVersandarten()
{
return array(
'pdf' => 'MahnungVersandPdf',
'mail' => 'MahnungVersandMail',
'druck' => 'MahnungVersandDruck',
'none' => 'MahnungVersandNone',
);
}
/**
* Alle Mahnstufen der Entity laden — inklusive der deaktivierten.
* fetchAllActive() filtert auf active = 1; die Setup-Seite muss aber auch
* abgeschaltete Stufen anzeigen und bearbeitbar halten.
*
* @param DoliDB $db Datenbank-Handler
* @param int $entity Entity
* @return MahnungStufe[] Aufsteigend nach Stufennummer
*/
function mahnungSetupAlleStufen($db, $entity)
{
$result = array();
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."mahnung_stufe";
$sql .= " WHERE entity = ".((int) $entity);
$sql .= " ORDER BY stufe ASC";
$resql = $db->query($sql);
if (!$resql) {
return $result;
}
$ids = array();
while ($obj = $db->fetch_object($resql)) {
$ids[] = (int) $obj->rowid;
}
$db->free($resql);
foreach ($ids as $id) {
$s = new MahnungStufe($db);
if ($s->fetch($id) > 0) {
$result[] = $s;
}
}
return $result;
}
/**
* Anzahl Mahnvorgänge je Stufennummer — Grundlage für die Löschsperre in der Liste.
*
* @param DoliDB $db Datenbank-Handler
* @return array Stufennummer => Anzahl Mahnvorgänge
*/
function mahnungSetupStufenBelegung($db)
{
$counts = array();
$sql = "SELECT stufe, COUNT(*) as nb FROM ".MAIN_DB_PREFIX."mahnung_mahnung";
$sql .= " WHERE entity IN (".getEntity('mahnung').")";
$sql .= " GROUP BY stufe";
$resql = $db->query($sql);
if (!$resql) {
return $counts;
}
while ($obj = $db->fetch_object($resql)) {
$counts[(int) $obj->stufe] = (int) $obj->nb;
}
$db->free($resql);
return $counts;
}
/**
* Zählt die Mahnvorgänge, die auf eine Stufennummer verweisen.
* Bei einem SQL-Fehler wird -1 geliefert — der Aufrufer sperrt dann das Löschen,
* damit im Zweifel keine Konfiguration verschwindet.
*
* @param DoliDB $db Datenbank-Handler
* @param int $stufe Stufennummer
* @return int Anzahl, -1 bei Fehler
*/
function mahnungSetupZaehleMahnungen($db, $stufe)
{
$sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."mahnung_mahnung";
$sql .= " WHERE stufe = ".((int) $stufe);
$sql .= " AND entity IN (".getEntity('mahnung').")";
$resql = $db->query($sql);
if (!$resql) {
return -1;
}
$obj = $db->fetch_object($resql);
$db->free($resql);
return $obj ? (int) $obj->nb : -1;
}
/**
* Betragsfeld aus dem POST lesen (Komma als Dezimaltrenner erlaubt) und prüfen.
* Wurde das Feld gar nicht abgeschickt (weil das Formular es nicht gerendert hat),
* bleibt der bisherige Wert erhalten.
*
* @param string $name Feldname im Formular
* @param float $current Bisheriger Wert
* @param string $bezeich Stufenbezeichnung für die Fehlermeldung
* @param string $label Feldbezeichnung für die Fehlermeldung
* @param string[] $errors Fehlerliste (by reference)
* @return float
*/
function mahnungSetupBetrag($name, $current, $bezeich, $label, &$errors)
{
global $langs;
if (!GETPOSTISSET($name)) {
return (float) $current;
}
$raw = trim((string) GETPOST($name, 'alphanohtml'));
if ($raw === '') {
return 0;
}
$val = str_replace(',', '.', $raw);
if (!is_numeric($val) || (float) $val < 0) {
$errors[] = $langs->transnoentities('MahnungStufeWertUngueltig', dol_escape_htmltag($bezeich), dol_escape_htmltag($label));
return (float) $current;
}
return (float) $val;
}
/**
* Ganzzahliges Tagesfeld aus dem POST lesen und auf >= 0 prüfen.
*
* @param string $name Feldname im Formular
* @param int $current Bisheriger Wert
* @param string $bezeich Stufenbezeichnung für die Fehlermeldung
* @param string $label Feldbezeichnung für die Fehlermeldung
* @param string[] $errors Fehlerliste (by reference)
* @return int
*/
function mahnungSetupTage($name, $current, $bezeich, $label, &$errors)
{
global $langs;
if (!GETPOSTISSET($name)) {
return (int) $current;
}
$raw = trim((string) GETPOST($name, 'alphanohtml'));
if ($raw === '') {
return 0;
}
if (!preg_match('/^\d+$/', $raw)) {
$errors[] = $langs->transnoentities('MahnungStufeWertUngueltig', dol_escape_htmltag($bezeich), dol_escape_htmltag($label));
return (int) $current;
}
return (int) $raw;
}
/**
* Zinssatz-Override aus dem POST lesen und auf den Bereich 0..100 prüfen.
* Leeres Feld = NULL = Standardzinssatz (Basiszins + Aufschlag) verwenden.
*
* @param string $name Feldname im Formular
* @param float|null $current Bisheriger Wert
* @param string $bezeich Stufenbezeichnung für die Fehlermeldung
* @param string $label Feldbezeichnung für die Fehlermeldung
* @param string[] $errors Fehlerliste (by reference)
* @return float|null
*/
function mahnungSetupZinssatz($name, $current, $bezeich, $label, &$errors)
{
global $langs;
$fallback = ($current === null || $current === '') ? null : (float) $current;
if (!GETPOSTISSET($name)) {
return $fallback;
}
$raw = trim((string) GETPOST($name, 'alphanohtml'));
if ($raw === '') {
return null;
}
$val = str_replace(',', '.', $raw);
if (!is_numeric($val) || (float) $val < 0 || (float) $val > 100) {
$errors[] = $langs->transnoentities('MahnungStufeZinssatzUngueltig', dol_escape_htmltag($bezeich), dol_escape_htmltag($label));
return $fallback;
}
return (float) $val;
}
/**
* Formularwerte einer Stufe aus dem POST übernehmen und prüfen.
*
* Kein Feld wird abhängig vom Erinnerungs-Flag verworfen oder überschrieben: alles,
* was im Formular steht, landet auch so in der DB. Ob eine Stufe die kostenlose
* Zahlungserinnerung ist, wirkt sich erst zur Anlagezeit aus (mahnungBaueVorgang()).
*
* Felder, die gar nicht gerendert wurden (z. B. weil ein anderer Block das Formular
* abgeschickt hat), kommen nicht im POST an — dann bleibt der bisherige Wert stehen.
* Deshalb überall GETPOSTISSET().
*
* @param MahnungStufe $s Zielobjekt (geladen oder frisch angelegt)
* @param string $prefix Feld-Prefix im Formular
* @param string $bezeich Stufenbezeichnung für Fehlermeldungen
* @param string[] $errors Fehlerliste (by reference)
* @return void
*/
function mahnungSetupApplyPost($s, $prefix, $bezeich, &$errors)
{
global $langs;
// Das Erinnerungs-Flag steuert nur noch die Anzeige und die Anlage-Logik —
// die übrigen Formularwerte werden davon unabhängig gelesen und gespeichert.
$s->ist_erinnerung = GETPOSTISSET($prefix.'ist_erinnerung') ? 1 : 0;
$s->active = GETPOSTISSET($prefix.'active') ? 1 : 0;
if (GETPOSTISSET($prefix.'label')) {
$s->label = trim((string) GETPOST($prefix.'label', 'alphanohtml'));
}
if (trim((string) $s->label) === '') {
$errors[] = $langs->transnoentities('MahnungStufeLabelFehlt', dol_escape_htmltag($bezeich));
}
$s->frist_tage = mahnungSetupTage($prefix.'frist_tage', $s->frist_tage, $bezeich, $langs->transnoentities('MahnungStufeFristTage'), $errors);
$s->neue_frist_tage = mahnungSetupTage($prefix.'neue_frist_tage', $s->neue_frist_tage, $bezeich, $langs->transnoentities('MahnungStufeNeueFristTage'), $errors);
// Kostenfelder werden IMMER unverändert aus dem Formular übernommen — auch wenn
// die Stufe als Zahlungserinnerung markiert ist. Früher wurden sie hier hart auf 0
// geschrieben; ein Fehlklick auf dem Häkchen hat damit Gebühren, Pauschale und
// beide Zinssatz-Overrides einer echten Mahnstufe unwiderruflich zerstört. Nötig
// war das Nullen ohnehin nie: mahnungBaueVorgang() in lib/mahnung_anlage.lib.php
// setzt Gebühr, §288-Pauschale, Zinsen und Vorstufenkosten bei einer Erinnerung
// zwingend auf 0, egal was in der Stufenkonfiguration steht.
//
// Bei einer Zahlungserinnerung blendet das Formular den kompletten KOSTEN-Block
// aus — aber nur per display:none, nie per "disabled". Versteckte Felder schickt
// der Browser normal mit, deaktivierte gar nicht. Als zweite Sicherung liegt im
// Block das versteckte Marker-Feld "kosten_present": kommt es nicht im POST an,
// wurde der Block überhaupt nicht gerendert und die gespeicherten Werte bleiben
// unangetastet. Ohne diese Klammer würde vor allem das Häkchen der §288-Pauschale
// still auf 0 fallen — ein nicht gerendertes Häkchen ist im POST nicht von einem
// abgewählten zu unterscheiden.
if (GETPOSTISSET($prefix.'kosten_present')) {
$s->mahngebuehr_b2c = mahnungSetupBetrag($prefix.'mahngebuehr_b2c', $s->mahngebuehr_b2c, $bezeich, $langs->transnoentities('MahnungStufeGebuehrB2C'), $errors);
$s->mahngebuehr_b2b = mahnungSetupBetrag($prefix.'mahngebuehr_b2b', $s->mahngebuehr_b2b, $bezeich, $langs->transnoentities('MahnungStufeGebuehrB2B'), $errors);
$s->pauschale_b2b_einmalig = GETPOSTISSET($prefix.'pauschale_b2b_einmalig') ? 1 : 0;
$s->zinssatz_b2c_uebersteuern = mahnungSetupZinssatz($prefix.'zinssatz_b2c', $s->zinssatz_b2c_uebersteuern, $bezeich, $langs->transnoentities('MahnungStufeZinssatzB2C'), $errors);
$s->zinssatz_b2b_uebersteuern = mahnungSetupZinssatz($prefix.'zinssatz_b2b', $s->zinssatz_b2b_uebersteuern, $bezeich, $langs->transnoentities('MahnungStufeZinssatzB2B'), $errors);
}
if (GETPOSTISSET($prefix.'versandart')) {
$va = GETPOST($prefix.'versandart', 'aZ09');
$s->versandart_default = array_key_exists($va, mahnungSetupVersandarten()) ? $va : 'pdf';
}
if (GETPOSTISSET($prefix.'pdf_intro')) {
$s->pdf_intro = GETPOST($prefix.'pdf_intro', 'restricthtml');
}
// E-Mail-Felder werden je nach Versandart nur ein-/ausgeblendet (display:none),
// abgeschickt werden sie trotzdem. Fehlen sie ausnahmsweise doch im POST, bleibt
// das gespeicherte Template unangetastet.
if (GETPOSTISSET($prefix.'email_subject')) {
$s->email_subject = GETPOST($prefix.'email_subject', 'alphanohtml');
}
if (GETPOSTISSET($prefix.'email_body')) {
$s->email_body = GETPOST($prefix.'email_body', 'restricthtml');
}
}
/**
* Geldbetrag für die Kopfzeile formatieren (deutsche Schreibweise, Euro-Zeichen).
*
* Das JS am Seitenende formatiert in mahnungGeld() identisch, damit die Kopfzeile
* beim Tippen nicht plötzlich anders aussieht als nach dem Neuladen. Wird hier
* etwas geändert, muss die JS-Funktion mitgezogen werden.
*
* @param float $wert Betrag
* @return string z. B. "1.234,50 €"
*/
function mahnungSetupGeld($wert)
{
return number_format((float) $wert, 2, ',', '.').' €';
}
/**
* Gebühren-Text der Kopfzeile. Sind B2C- und B2B-Gebühr gleich, steht nur ein
* Betrag da; weichen sie ab, werden beide gezeigt (B2C zuerst).
*
* @param MahnungStufe $s Stufe
* @return string
*/
function mahnungSetupKopfGebuehr($s)
{
$b2c = (float) $s->mahngebuehr_b2c;
$b2b = (float) $s->mahngebuehr_b2b;
if (abs($b2b - $b2c) < 0.005) {
return mahnungSetupGeld($b2c);
}
return mahnungSetupGeld($b2c).' / '.mahnungSetupGeld($b2b);
}
/**
* Frist-Text der Kopfzeile: "sofort" bei 0 Tagen, sonst "nach N Tagen".
*
* @param int $tage Wert aus frist_tage
* @return string HTML-kodiert (trans() konvertiert bereits)
*/
function mahnungSetupKopfFrist($tage)
{
global $langs;
$tage = (int) $tage;
if ($tage <= 0) {
return $langs->trans('MahnungStufeKopfSofort');
}
return $langs->trans('MahnungStufeKopfNachTagen', $tage);
}
/**
* Eine Formularzeile im Stufen-Layout ausgeben (Bezeichnung links, Eingabe rechts).
* Bewusst kein <tr>: die Stufen stecken in <details>-Blöcken, und HTML erlaubt kein
* <details> um Tabellenzeilen herum. Das Zwei-Spalten-Raster macht CSS (grid).
*
* @param string $label Bereits übersetzte Feldbezeichnung
* @param string $inputHtml Fertiges HTML des Eingabefelds (Werte dort escapen!)
* @param string $cssClass Zusätzliche CSS-Klassen der Zeile
* @param string $style Zusätzliches style-Attribut (z. B. "display:none;")
* @return void
*/
function mahnungSetupFeldZeile($label, $inputHtml, $cssClass = '', $style = '')
{
print '<div class="mahnung_feld'.($cssClass !== '' ? ' '.$cssClass : '').'"'.($style !== '' ? ' style="'.$style.'"' : '').'>';
print '<div class="mahnung_feldname">'.$label.'</div>';
print '<div class="mahnung_feldwert">'.$inputHtml.'</div>';
print '</div>';
}
/**
* Zwischenüberschrift eines Blocks (FRISTEN / KOSTEN / VERSAND).
*
* @param string $text Bereits übersetzte Überschrift
* @return void
*/
function mahnungSetupBlockTitel($text)
{
print '<div class="mahnung_blocktitel">'.$text.'</div>';
}
/**
* Die zugeklappte Kopfzeile einer Mahnstufe ausgeben (<summary> des Akkordeons).
*
* Zeigt Stufennummer, Bezeichnung und kompakt die Eckdaten
* "Betrag · Versandart · Frist". Bei einer Zahlungserinnerung steht "kostenlos"
* statt eines Betrags, zusätzlich gibt es ein Abzeichen.
*
* In die Kopfzeile kommen bewusst KEINE Bedienelemente: ein Klick auf ein
* Eingabefeld oder einen Link innerhalb eines <summary> klappt in den Browsern
* zusätzlich den Abschnitt um. Aktiv-Häkchen und Löschen sitzen deshalb woanders.
* Der Hinweis auf die Löschsperre ist dagegen reiner Text ohne Klickverhalten und
* darf mit in die Kopfzeile — dort fließt er im Layout mit, statt sich als
* absolut positioniertes Element über die Eckdaten zu legen.
*
* @param MahnungStufe $s Stufe mit den anzuzeigenden Werten
* @param string $key Schlüssel für die CSS-Klassen des Umschalt-JS
* @param int $nbUse Anzahl Mahnvorgänge auf dieser Stufennummer (Löschsperre)
* @return void
*/
function mahnungSetupPrintKopf($s, $key, $nbUse = 0)
{
global $langs;
$k = dol_escape_htmltag((string) $key);
$istErinnerung = !empty($s->ist_erinnerung);
$vaListe = mahnungSetupVersandarten();
$va = $s->versandart_default ?: 'pdf';
$vaText = isset($vaListe[$va]) ? $langs->trans($vaListe[$va]) : $langs->trans('MahnungVersandPdf');
print '<summary class="mahnung_stufe_kopf">';
print '<span class="mahnung_kopf_nr" title="'.$langs->trans('MahnungStufeNummer').'">'.((int) $s->stufe).'</span>';
print '<span class="mahnung_kopf_label mahnung_kopflabel_'.$k.'">'.dol_escape_htmltag((string) $s->label).'</span>';
// Abzeichen "Zahlungserinnerung" — immer gerendert, per JS ein-/ausgeblendet
print '<span class="badge mahnung_badge mahnung_badge_erinnerung mahnung_kopfbadge_'.$k.'"'.($istErinnerung ? '' : ' style="display:none;"').'>';
print $langs->trans('MahnungStufeIstErinnerung').'</span>';
// Abzeichen "inaktiv" — ebenfalls immer gerendert, damit das Aktiv-Häkchen im
// aufgeklappten Zustand die Kopfzeile ohne Neuladen mitziehen kann
print '<span class="badge mahnung_badge mahnung_kopfinaktiv_'.$k.'"'.(empty($s->active) ? '' : ' style="display:none;"').'>';
print $langs->trans('MahnungStufeInaktiv').'</span>';
// Löschsperre: verweist mindestens ein Mahnvorgang auf diese Stufennummer, gibt es
// statt des Papierkorbs nur diesen Hinweis. Die vollständige Begründung samt Anzahl
// steht im Tooltip.
if ($nbUse > 0) {
print '<span class="badge mahnung_badge opacitymedium" title="'.$langs->trans('MahnungStufeInVerwendung', $nbUse).'">';
print $langs->trans('MahnungStufeNichtLoeschbarKurz').'</span>';
}
print '<span class="mahnung_kopf_meta opacitymedium">';
print '<span class="mahnung_kopfkosten_'.$k.'" title="'.$langs->trans('MahnungStufeKopfGebuehrTitel').'">';
print $istErinnerung ? $langs->trans('MahnungStufeKopfKostenlos') : dol_escape_htmltag(mahnungSetupKopfGebuehr($s));
print '</span> · ';
print '<span class="mahnung_kopfversand_'.$k.'">'.$vaText.'</span> · ';
print '<span class="mahnung_kopffrist_'.$k.'">'.mahnungSetupKopfFrist($s->frist_tage).'</span>';
print '</span>';
print '</summary>';
}
/**
* Die Eingabefelder einer Mahnstufe ausgeben (gemeinsam für Bearbeiten und Anlegen).
*
* Aufbau: zuerst Bezeichnung, Erinnerungs-Häkchen und Aktiv-Schalter, danach die
* drei Blöcke FRISTEN, KOSTEN und VERSAND. Selten gebrauchte Felder (Zinssatz-
* Overrides, Textbausteine) stecken in eigenen, zugeklappten Unterabschnitten —
* die klappen automatisch auf, sobald darin ein Wert vom Standard abweicht.
*
* @param MahnungStufe $s Stufe mit den anzuzeigenden Werten
* @param string $prefix Feld-Prefix im Formular (z. B. "stufe12_" oder "new_")
* @param string $key Schlüssel für die CSS-Klassen des Umschalt-JS (rowid oder "new")
* @return void
*/
function mahnungSetupPrintFelder($s, $prefix, $key)
{
global $langs;
$k = dol_escape_htmltag((string) $key);
$istErinnerung = !empty($s->ist_erinnerung);
$vaListe = mahnungSetupVersandarten();
$va = $s->versandart_default ?: 'pdf';
if (!isset($vaListe[$va])) {
$va = 'pdf';
}
// Bei einer Zahlungserinnerung verschwindet der komplette KOSTEN-Block samt
// Zwischenüberschrift — aber ausschließlich per display:none, NIE per "disabled".
// Versteckte Felder schickt der Browser normal mit, deaktivierte gar nicht: das
// Bulk-Update (action save_stufen) würde die gespeicherte Gebührenkonfiguration
// sonst beim nächsten Speichern mit Nullen überschreiben, sobald jemand die Stufe
// als Erinnerung markiert. Zusätzlich steckt im Block das versteckte Marker-Feld
// "kosten_present" — fehlt es im POST, fasst mahnungSetupApplyPost() die
// Kostenwerte gar nicht erst an. Die Werte gelten wieder, sobald das Häkchen weg ist.
$kostenStyle = $istErinnerung ? ' style="display:none;"' : '';
$hinweisStyle = $istErinnerung ? '' : ' style="display:none;"';
// E-Mail-Felder: sichtbar bei Versandart "E-Mail" ODER wenn die Stufe eine
// Zahlungserinnerung ist — die geht laut mahnungBaueVorgang() zwingend per E-Mail
// raus, ganz gleich was als Versandart-Default eingestellt ist. Ohne diese
// Ausnahme wären Betreff und Text einer Erinnerung nicht erreichbar.
$mailStyle = ($va === 'mail' || $istErinnerung) ? '' : 'display:none;';
// Der PDF-Einleitungstext ist das Gegenstück: geht die Stufe per E-Mail raus,
// entsteht überhaupt kein Mahnschreiben, in das ein Einleitungstext könnte.
// Für eine Zahlungserinnerung gilt das ohnehin — die hat nie ein eigenes PDF,
// angehängt wird die unveränderte Original-Rechnung.
// Ausgeblendet statt entfernt: der gespeicherte Text bleibt im Formular und
// damit erhalten, falls die Versandart später wieder auf Post umgestellt wird.
$pdfStyle = ($va === 'mail' || $istErinnerung) ? 'display:none;' : '';
// Standard-Zinssatz für Platzhalter und Hilfetexte (Basiszins + Aufschlag)
$basisVal = (float) getDolGlobalString('MAHNUNG_BASISZINS', '1.27');
$aufB2c = (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2C', '5.0');
$aufB2b = (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2B', '9.0');
$stdB2c = $basisVal + $aufB2c;
$stdB2b = $basisVal + $aufB2b;
$zinsB2c = ($s->zinssatz_b2c_uebersteuern !== null && $s->zinssatz_b2c_uebersteuern !== '') ? (string) (float) $s->zinssatz_b2c_uebersteuern : '';
$zinsB2b = ($s->zinssatz_b2b_uebersteuern !== null && $s->zinssatz_b2b_uebersteuern !== '') ? (string) (float) $s->zinssatz_b2b_uebersteuern : '';
// Unterabschnitte starten zugeklappt — außer es steckt bereits etwas darin, das
// vom Standard abweicht. Sonst findet man seine eigenen Einstellungen nicht wieder.
$zinsOffen = ($zinsB2c !== '' || $zinsB2b !== '');
$texteOffen = (trim((string) $s->pdf_intro) !== ''
|| trim((string) $s->email_subject) !== ''
|| trim((string) $s->email_body) !== '');
// --- Bezeichnung, Erinnerungs-Häkchen, Aktiv-Schalter -----------------------
// Das "required" bleibt am Bezeichnungsfeld: Damit die Browser-Meldung nicht an
// einem zugeklappten <details> verschluckt wird (Chrome sendet dann kommentarlos
// nicht ab), klappt das JS am Seitenende beim invalid-Ereignis alle übergeordneten
// Abschnitte auf. Serverseitig prüft mahnungSetupApplyPost() zusätzlich.
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeLabel'),
'<input type="text" class="mahnung_label" data-mahnungkey="'.$k.'" name="'.$prefix.'label" size="40"'
.' value="'.dol_escape_htmltag((string) $s->label).'" required>'
);
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeIstErinnerung'),
'<input type="checkbox" id="mahnung_erinnerung_'.$k.'" class="mahnung_erinnerung" data-mahnungkey="'.$k.'"'
.' name="'.$prefix.'ist_erinnerung" value="1"'.($istErinnerung ? ' checked' : '').'> '
.'<span class="opacitymedium">'.$langs->trans('MahnungStufeIstErinnerungHelp').'</span>'
);
mahnungSetupFeldZeile(
$langs->trans('Active'),
'<input type="checkbox" class="mahnung_active" data-mahnungkey="'.$k.'"'
.' name="'.$prefix.'active" value="1"'.(!empty($s->active) ? ' checked' : '').'>'
);
// --- Block FRISTEN ----------------------------------------------------------
mahnungSetupBlockTitel($langs->trans('MahnungBlockFristen'));
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeFristTage'),
'<input type="number" min="0" class="mahnung_frist" data-mahnungkey="'.$k.'"'
.' name="'.$prefix.'frist_tage" size="6" value="'.((int) $s->frist_tage).'">'
);
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeNeueFristTage'),
'<input type="number" min="0" name="'.$prefix.'neue_frist_tage" size="6" value="'.((int) $s->neue_frist_tage).'">'
);
// --- Block KOSTEN (bei einer Zahlungserinnerung komplett ausgeblendet) -------
print '<div class="mahnung_block mahnung_kostenblock_'.$k.'"'.$kostenStyle.'>';
print '<input type="hidden" name="'.$prefix.'kosten_present" value="1">';
mahnungSetupBlockTitel($langs->trans('MahnungBlockKosten'));
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeGebuehrB2C'),
'<input type="text" class="mahnung_gebuehrb2c" data-mahnungkey="'.$k.'"'
.' name="'.$prefix.'mahngebuehr_b2c" size="8" value="'.((float) $s->mahngebuehr_b2c).'"> EUR'
);
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeGebuehrB2B'),
'<input type="text" class="mahnung_gebuehrb2b" data-mahnungkey="'.$k.'"'
.' name="'.$prefix.'mahngebuehr_b2b" size="8" value="'.((float) $s->mahngebuehr_b2b).'"> EUR'
);
mahnungSetupFeldZeile(
$langs->trans('MahnungPauschaleB2B').' (§288 Abs. 5)',
'<input type="checkbox" name="'.$prefix.'pauschale_b2b_einmalig" value="1"'.(!empty($s->pauschale_b2b_einmalig) ? ' checked' : '').'>'
);
// Zinssatz-Overrides: selten gebraucht, deshalb hinter einem eigenen Abschnitt.
// Ist bereits ein Override gesetzt, startet er aufgeklappt.
print '<details class="mahnung_unterblock"'.($zinsOffen ? ' open' : '').'>';
print '<summary>'.$langs->trans('MahnungBlockZinssatzOverride').'</summary>';
print '<div class="mahnung_unterblock_body">';
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeZinssatzB2C'),
'<input type="text" name="'.$prefix.'zinssatz_b2c" size="8" placeholder="'.number_format($stdB2c, 2, ',', '.').'"'
.' value="'.dol_escape_htmltag($zinsB2c).'"> %'
.' <span class="opacitymedium">'.$langs->trans('MahnungZinssatzHelpB2C', number_format($basisVal, 2, ',', '.'), number_format($aufB2c, 1, ',', '.'), number_format($stdB2c, 2, ',', '.')).'</span>'
);
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeZinssatzB2B'),
'<input type="text" name="'.$prefix.'zinssatz_b2b" size="8" placeholder="'.number_format($stdB2b, 2, ',', '.').'"'
.' value="'.dol_escape_htmltag($zinsB2b).'"> %'
.' <span class="opacitymedium">'.$langs->trans('MahnungZinssatzHelpB2B', number_format($basisVal, 2, ',', '.'), number_format($aufB2b, 1, ',', '.'), number_format($stdB2b, 2, ',', '.')).'</span>'
);
print '</div>';
print '</details>';
print '</div>';
// Ersatz für den ausgeblendeten Kostenblock: eine Zeile, dass die Stufe nichts kostet
print '<div class="mahnung_block mahnung_kostenhint_'.$k.'"'.$hinweisStyle.'>';
mahnungSetupBlockTitel($langs->trans('MahnungBlockKosten'));
print '<div class="mahnung_hinweis opacitymedium">'.$langs->trans('MahnungStufeErinnerungKostenHinweis').'</div>';
print '</div>';
// --- Block VERSAND ----------------------------------------------------------
mahnungSetupBlockTitel($langs->trans('MahnungBlockVersand'));
$versandHtml = '<select id="mahnung_versandart_'.$k.'" class="mahnung_versandart" data-mahnungkey="'.$k.'" name="'.$prefix.'versandart">';
foreach ($vaListe as $v => $tx) {
$versandHtml .= '<option value="'.$v.'"'.($va === $v ? ' selected' : '').'>'.$langs->trans($tx).'</option>';
}
$versandHtml .= '</select>';
// Hinweis immer rendern, aber nur zeigen wenn Erinnerung + Versandart != E-Mail.
// Das JS schaltet ihn beim Umstellen live um — sonst passte der Hinweis nach dem
// Anhaken des Erinnerungs-Häkchens erst nach einem Neuladen wieder zur Auswahl.
$versandHtml .= ' <span class="mahnung_versandhint_'.$k.' warning"'.(($istErinnerung && $va !== 'mail') ? '' : ' style="display:none;"').'>';
$versandHtml .= $langs->trans('MahnungStufeErinnerungVersandHinweis').'</span>';
mahnungSetupFeldZeile($langs->trans('MahnungStufeVersandartDefault'), $versandHtml);
// Textbausteine: ebenfalls selten angefasst, deshalb zugeklappt. Ist bereits ein
// Text hinterlegt, startet der Abschnitt offen.
print '<details class="mahnung_unterblock"'.($texteOffen ? ' open' : '').'>';
print '<summary>'.$langs->trans('MahnungBlockTexte').'</summary>';
print '<div class="mahnung_unterblock_body">';
mahnungSetupFeldZeile(
$langs->trans('MahnungStufePdfIntro'),
'<textarea name="'.$prefix.'pdf_intro" cols="80" rows="3">'.dol_escape_htmltag((string) $s->pdf_intro).'</textarea>',
'mahnung_pdfrow_'.$k,
$pdfStyle
);
// Welche Platzhalter es gibt, stand bisher nur als Kommentar in der Sprachdatei —
// im Formular war es Ratesache. Beide Schreibweisen werden unterstützt.
mahnungSetupFeldZeile(
'',
'<span class="opacitymedium">'.$langs->trans('MahnungPlatzhalterHilfe').'</span>',
'mahnung_emailrow_'.$k,
$mailStyle
);
// E-Mail-Felder bei Versandart "E-Mail" oder bei einer Zahlungserinnerung
// (Umschalten ohne Neuladen per JS unten — siehe syncEmailRows()).
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeEmailSubject'),
'<input type="text" name="'.$prefix.'email_subject" size="80" value="'.dol_escape_htmltag((string) $s->email_subject).'">',
'mahnung_emailrow_'.$k,
$mailStyle
);
// Mailtext mit dem gewohnten Dolibarr-Editor pflegen (WYSIWYG, wenn
// FCKEDITOR_ENABLE_MAIL gesetzt ist — dieselbe Konstante und dieselbe
// Toolbar wie bei Dolibarrs eigenen Mailvorlagen, siehe
// admin/mails_templates.php). Wer lieber Klartext schreibt, bekommt bei
// abgeschalteter Konstante weiterhin ein einfaches Textfeld.
$editorEmailBody = new DolEditor(
$prefix.'email_body',
(string) $s->email_body,
'',
220,
'dolibarr_mailings',
'In',
false,
true,
(bool) getDolGlobalString('FCKEDITOR_ENABLE_MAIL'),
ROWS_5,
'90%'
);
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeEmailBody'),
$editorEmailBody->Create(1),
'mahnung_emailrow_'.$k,
$mailStyle
);
print '</div>';
print '</details>';
}
// ---------------------------------------------------------------
// POST: Allgemeine Konstanten speichern
// ---------------------------------------------------------------
if ($action === 'save_consts' && $user->hasRight('mahnung', 'setup')) {
$basis = str_replace(',', '.', GETPOST('MAHNUNG_BASISZINS', 'alphanohtml'));
$b2c = str_replace(',', '.', GETPOST('MAHNUNG_AUFSCHLAG_B2C', 'alphanohtml'));
$b2b = str_replace(',', '.', GETPOST('MAHNUNG_AUFSCHLAG_B2B', 'alphanohtml'));
$pau = str_replace(',', '.', GETPOST('MAHNUNG_PAUSCHALE_B2B', 'alphanohtml'));
$topic = GETPOST('MAHNUNG_NTFY_TOPIC', 'alphanohtml');
$boxmax = (int) GETPOST('MAHNUNG_BOX_MAXLINES', 'int');
// Absender der Erinnerungs-Mails. Leer = Firmenadresse bzw. globale
// Dolibarr-Absenderadresse (siehe mahnungAbsender()).
$senderMail = trim((string) GETPOST('MAHNUNG_EMAIL_SENDER', 'alphanohtml'));
$senderName = trim((string) GETPOST('MAHNUNG_EMAIL_SENDER_NAME', 'alphanohtml'));
if ($senderMail !== '' && !isValidEmail($senderMail)) {
// Eine ungültige Absenderadresse würde jeden Versand scheitern lassen —
// lieber gar nicht erst speichern.
setEventMessages($langs->trans('MahnungSenderMailUngueltig', $senderMail), null, 'errors');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
dolibarr_set_const($db, 'MAHNUNG_BASISZINS', (string) (float) $basis, 'chaine', 0, '', 0);
dolibarr_set_const($db, 'MAHNUNG_AUFSCHLAG_B2C', (string) (float) $b2c, 'chaine', 0, '', 0);
dolibarr_set_const($db, 'MAHNUNG_AUFSCHLAG_B2B', (string) (float) $b2b, 'chaine', 0, '', 0);
dolibarr_set_const($db, 'MAHNUNG_PAUSCHALE_B2B', (string) (float) $pau, 'chaine', 0, '', 0);
dolibarr_set_const($db, 'MAHNUNG_NTFY_TOPIC', (string) $topic, 'chaine', 0, '', $conf->entity);
// Widget-Anzeige: max. Zeilen offener Rechnungen (0 = alle)
dolibarr_set_const($db, 'MAHNUNG_BOX_MAXLINES', (string) $boxmax, 'chaine', 0, '', 0);
dolibarr_set_const($db, 'MAHNUNG_EMAIL_SENDER', $senderMail, 'chaine', 0, '', $conf->entity);
dolibarr_set_const($db, 'MAHNUNG_EMAIL_SENDER_NAME', $senderName, 'chaine', 0, '', $conf->entity);
setEventMessages($langs->trans('MahnungSettingsSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
// ---------------------------------------------------------------
// POST: Stufen speichern (Bulk-Update über alle konfigurierten Stufen)
// ---------------------------------------------------------------
if ($action === 'save_stufen' && $user->hasRight('mahnung', 'setup')) {
$errors = array();
$toSave = array();
foreach (mahnungSetupAlleStufen($db, $conf->entity) as $s) {
$prefix = 'stufe'.((int) $s->id).'_';
// Block war gar nicht im Formular (z. B. Stufe parallel angelegt) -> nicht anfassen
if (!GETPOSTISSET($prefix.'present')) {
continue;
}
mahnungSetupApplyPost($s, $prefix, (string) $s->stufe, $errors);
$toSave[(int) $s->id] = $s;
}
if (!empty($errors)) {
// Nichts speichern — die eingegebenen Werte bleiben im Formular stehen
setEventMessages('', $errors, 'errors');
$stufenPost = $toSave;
} else {
$ok = true;
foreach ($toSave as $s) {
if ($s->update($user) <= 0) {
$ok = false;
setEventMessages($s->error ?: $langs->trans('MahnungSpeichernFehlgeschlagen'), null, 'errors');
}
}
if ($ok) {
setEventMessages($langs->trans('MahnungSettingsSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
$stufenPost = $toSave;
}
}
// ---------------------------------------------------------------
// POST: Neue Mahnstufe anlegen
// ---------------------------------------------------------------
if ($action === 'add_stufe' && $user->hasRight('mahnung', 'setup')) {
$errors = array();
$neu = new MahnungStufe($db);
$neu->entity = $conf->entity;
// Stufennummer: frei wählbar, aber TINYINT -> 0..127
$rawStufe = trim((string) GETPOST('new_stufe', 'alphanohtml'));
if (!preg_match('/^\d+$/', $rawStufe) || (int) $rawStufe > 127) {
$errors[] = $langs->transnoentities('MahnungStufeNummerUngueltig');
$neu->stufe = 0;
} else {
$neu->stufe = (int) $rawStufe;
}
mahnungSetupApplyPost($neu, 'new_', ($rawStufe !== '' ? $rawStufe : '?'), $errors);
if (!empty($errors)) {
setEventMessages('', $errors, 'errors');
$neuStufePost = $neu;
} else {
$resCreate = $neu->create($user);
if ($resCreate > 0) {
setEventMessages($langs->trans('MahnungStufeAngelegt'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
// -2 = Stufennummer bereits vergeben (MahnungStufeNummerVergeben)
setEventMessages($neu->error ?: $langs->trans('MahnungSpeichernFehlgeschlagen'), null, 'errors');
$neuStufePost = $neu;
}
}
// ---------------------------------------------------------------
// POST: Mahnstufe löschen (bestätigt über den formconfirm-Dialog weiter unten)
// ---------------------------------------------------------------
if ($action === 'confirm_delete_stufe' && $confirm === 'yes' && $rowid > 0 && $user->hasRight('mahnung', 'setup')) {
$del = new MahnungStufe($db);
if ($del->fetch($rowid) <= 0) {
setEventMessages($langs->trans('MahnungStufeNichtGefunden'), null, 'errors');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
// Löschsperre serverseitig: verweist auch nur ein Mahnvorgang auf diese
// Stufennummer, darf die Konfiguration nicht verschwinden — sonst verlieren
// Bestandsmahnungen ihre Grundlage. Dann ist "deaktivieren" der richtige Weg.
$nbUse = mahnungSetupZaehleMahnungen($db, (int) $del->stufe);
if ($nbUse < 0) {
setEventMessages($langs->trans('MahnungStufePruefungFehlgeschlagen'), null, 'errors');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
if ($nbUse > 0) {
setEventMessages($langs->transnoentities('MahnungStufeNichtLoeschbarAnzahl', (int) $del->stufe, $nbUse), null, 'errors');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
if ($del->delete($user) > 0) {
setEventMessages($langs->trans('MahnungStufeGeloescht'), null, 'mesgs');
} else {
setEventMessages($del->error ?: $langs->trans('MahnungSpeichernFehlgeschlagen'), null, 'errors');
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
// ---------------------------------------------------------------
// ODT-Upload / Template-Löschen (actions_setmoduleoptions.inc.php)
// Muss vor llxHeader() stehen, da es header()-Redirects macht.
// ---------------------------------------------------------------
include_once DOL_DOCUMENT_ROOT.'/core/actions_setmoduleoptions.inc.php';
// ---------------------------------------------------------------
// Dokumentenmodell-Aktionen (vor View, da header()-Redirect)
// ---------------------------------------------------------------
if ($action === 'setdoc' && $user->hasRight('mahnung', 'setup')) {
$newModel = GETPOST('value', 'alphanohtml');
if (!empty($newModel)) {
dolibarr_set_const($db, 'MAHNUNG_ADDON_PDF', $newModel, 'chaine', 0, '', $conf->entity);
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
if ($action === 'set' && $user->hasRight('mahnung', 'setup')) {
$newModel = GETPOST('value', 'alphanohtml');
$sql = "INSERT INTO ".MAIN_DB_PREFIX."document_model (nom, type, entity, libelle) VALUES ('".$db->escape($newModel)."', 'mahnung', ".((int) $conf->entity).", '".$db->escape($newModel)."')";
$db->query($sql);
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
if ($action === 'del' && $user->hasRight('mahnung', 'setup')) {
$newModel = GETPOST('value', 'alphanohtml');
$sql = "DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = '".$db->escape($newModel)."' AND type = 'mahnung' AND entity = ".((int) $conf->entity);
$db->query($sql);
if (getDolGlobalString('MAHNUNG_ADDON_PDF') === $newModel) {
dolibarr_del_const($db, 'MAHNUNG_ADDON_PDF', $conf->entity);
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
// ---------------------------------------------------------------
// View
// ---------------------------------------------------------------
llxHeader('', $langs->trans('MahnungSetupPage'));
$setupExtra = '<a href="tracking_patterns.php" class="button smallpaddingimp">'.dol_escape_htmltag($langs->trans('MahnungTrackingPatternsSetup')).'</a>';
print load_fiche_titre($langs->trans('MahnungSetupPage'), $setupExtra, 'fa-envelope-open-text');
print '<span class="opacitymedium">'.$langs->trans('MahnungSetupDescription').'</span><br><br>';
// --- Block: Konstanten -------------------------------------------------------
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="save_consts">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre"><th colspan="2">'.$langs->trans('MahnungSetup').'</th></tr>';
print '<tr class="oddeven"><td>'.$langs->trans('MahnungBasiszins').'</td>';
print '<td><input type="text" name="MAHNUNG_BASISZINS" size="8" value="'.dol_escape_htmltag(getDolGlobalString('MAHNUNG_BASISZINS', '1.27')).'"> %';
print ' <span class="opacitymedium">'.$langs->trans('MahnungBasiszinsHelp').'</span></td></tr>';
print '<tr class="oddeven"><td>'.$langs->trans('MahnungAufschlagB2C').'</td>';
print '<td><input type="text" name="MAHNUNG_AUFSCHLAG_B2C" size="8" value="'.dol_escape_htmltag(getDolGlobalString('MAHNUNG_AUFSCHLAG_B2C', '5.0')).'"> %</td></tr>';
print '<tr class="oddeven"><td>'.$langs->trans('MahnungAufschlagB2B').'</td>';
print '<td><input type="text" name="MAHNUNG_AUFSCHLAG_B2B" size="8" value="'.dol_escape_htmltag(getDolGlobalString('MAHNUNG_AUFSCHLAG_B2B', '9.0')).'"> %</td></tr>';
print '<tr class="oddeven"><td>'.$langs->trans('MahnungPauschaleB2BLabel').'</td>';
print '<td><input type="text" name="MAHNUNG_PAUSCHALE_B2B" size="8" value="'.dol_escape_htmltag(getDolGlobalString('MAHNUNG_PAUSCHALE_B2B', '40.00')).'"> EUR</td></tr>';
print '<tr class="oddeven"><td>'.$langs->trans('MahnungNtfyTopic').'</td>';
print '<td><input type="text" name="MAHNUNG_NTFY_TOPIC" size="40" value="'.dol_escape_htmltag(getDolGlobalString('MAHNUNG_NTFY_TOPIC', 'vk-builds')).'">';
print ' <span class="opacitymedium">'.$langs->trans('MahnungNtfyTopicHelp').'</span></td></tr>';
// Widget: max. Anzahl angezeigter offener Rechnungen (0 = alle)
$curBoxMax = getDolGlobalInt('MAHNUNG_BOX_MAXLINES', 0);
print '<tr class="oddeven"><td>'.$langs->trans('MahnungBoxMaxLines').'</td>';
print '<td><select name="MAHNUNG_BOX_MAXLINES">';
$boxMaxOpts = array(0 => $langs->trans('MahnungBoxMaxLinesAll'), 5 => '5', 10 => '10', 20 => '20', 30 => '30', 50 => '50');
foreach ($boxMaxOpts as $v => $lbl) {
print '<option value="'.$v.'"'.($curBoxMax === $v ? ' selected' : '').'>'.$lbl.'</option>';
}
print '</select>';
print ' <span class="opacitymedium">'.$langs->trans('MahnungBoxMaxLinesHelp').'</span></td></tr>';
// Absender der Erinnerungs-Mails. Leer lassen = Firmenadresse aus Start > Einstellungen
// > Firma/Stiftung, sonst die globale Dolibarr-Absenderadresse.
print '<tr class="oddeven"><td>'.$langs->trans('MahnungSenderMail').'</td>';
print '<td><input type="email" name="MAHNUNG_EMAIL_SENDER" size="40" value="'.dol_escape_htmltag(getDolGlobalString('MAHNUNG_EMAIL_SENDER')).'" placeholder="'.dol_escape_htmltag((string) (isset($mysoc->email) ? $mysoc->email : '')).'">';
print ' <span class="opacitymedium">'.$langs->trans('MahnungSenderMailHelp').'</span></td></tr>';
print '<tr class="oddeven"><td>'.$langs->trans('MahnungSenderName').'</td>';
print '<td><input type="text" name="MAHNUNG_EMAIL_SENDER_NAME" size="40" value="'.dol_escape_htmltag(getDolGlobalString('MAHNUNG_EMAIL_SENDER_NAME')).'" placeholder="'.dol_escape_htmltag((string) (isset($mysoc->name) ? $mysoc->name : '')).'">';
print ' <span class="opacitymedium">'.$langs->trans('MahnungSenderNameHelp').'</span></td></tr>';
print '</table>';
print '<br><div class="center"><input type="submit" class="button" value="'.$langs->trans('Save').'"></div>';
print '</form>';
// --- Block: Stufen -----------------------------------------------------------
// Lokales CSS (kein CDN) für das Mahnstufen-Akkordeon. Bewusst ohne feste
// Theme-Farben: Hintergründe sind neutrale Grau-Schleier mit Alpha, Rahmen und
// Pfeil nutzen currentColor, die Schrift erbt die Farbe des Themes. Damit bleibt
// alles sowohl im hellen Standard-Theme als auch im dunklen awl-dark lesbar.
// Die Dolibarr-Klasse "badge" liefert nur die Grundform — Farbe und Hintergrund
// setzt "mahnung_badge" bewusst neu, weil Dolibarrs Badge-Varianten weißen Text
// mitbringen, der auf einem hellen Untergrund verschwinden würde.
print '<style>
.mahnung_stufe_wrap { position: relative; margin-bottom: 6px; }
.mahnung_stufe { border: 1px solid rgba(128, 128, 128, 0.35); border-radius: 4px; }
.mahnung_stufe > summary { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; cursor: pointer; padding: 8px 44px 8px 10px; background: rgba(128, 128, 128, 0.10); list-style: none; }
.mahnung_stufe > summary::-webkit-details-marker { display: none; }
.mahnung_stufe > summary:hover { background: rgba(128, 128, 128, 0.18); }
.mahnung_stufe > summary::before { content: ""; flex: 0 0 auto; width: 0; height: 0; border-left: 6px solid currentColor; border-top: 5px solid transparent; border-bottom: 5px solid transparent; transition: transform 0.12s ease; }
.mahnung_stufe[open] > summary::before { transform: rotate(90deg); }
.mahnung_stufe[open] > summary { border-bottom: 1px solid rgba(128, 128, 128, 0.25); }
.mahnung_kopf_nr { display: inline-block; min-width: 1.9em; text-align: center; font-weight: bold; padding: 0 4px; border-radius: 3px; border: 1px solid rgba(128, 128, 128, 0.45); }
.mahnung_kopf_label { font-weight: bold; }
.mahnung_kopf_meta { margin-left: auto; }
.mahnung_stufe_aktion { position: absolute; top: 9px; right: 10px; line-height: 1; z-index: 2; }
.mahnung_stufe_body { padding: 2px 0 10px; }
.mahnung_stufe_neu { margin-bottom: 6px; }
.mahnung_blocktitel { margin-top: 8px; padding: 9px 10px 4px; border-top: 1px solid rgba(128, 128, 128, 0.25); font-weight: bold; font-size: 0.86em; text-transform: uppercase; letter-spacing: 0.07em; opacity: 0.8; }
.mahnung_feld { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 3px 14px; align-items: start; padding: 6px 10px; }
.mahnung_feld + .mahnung_feld { border-top: 1px solid rgba(128, 128, 128, 0.14); }
.mahnung_feldname { padding-top: 3px; }
.mahnung_feldwert input, .mahnung_feldwert select { max-width: 100%; }
.mahnung_feldwert textarea { width: 100%; max-width: 100%; box-sizing: border-box; }
.mahnung_hinweis { padding: 4px 10px 6px; }
.mahnung_unterblock { margin: 6px 10px 2px; border: 1px dashed rgba(128, 128, 128, 0.35); border-radius: 4px; }
.mahnung_unterblock > summary { cursor: pointer; padding: 6px 10px; }
.mahnung_unterblock > summary:hover { background: rgba(128, 128, 128, 0.10); }
.mahnung_unterblock_body { border-top: 1px solid rgba(128, 128, 128, 0.22); padding: 2px 0 4px; }
.mahnung_badge { display: inline-block; padding: 1px 8px; border-radius: 9px; font-size: 0.85em; font-weight: normal; line-height: 1.6; border: 1px solid rgba(128, 128, 128, 0.45); background: rgba(128, 128, 128, 0.14); color: inherit; white-space: nowrap; }
.mahnung_badge_erinnerung { border-color: rgba(70, 160, 100, 0.75); background: rgba(70, 160, 100, 0.20); }
@media (max-width: 920px) {
.mahnung_feld { grid-template-columns: minmax(0, 1fr); }
.mahnung_kopf_meta { margin-left: 0; width: 100%; }
}
</style>';
$stufen = mahnungSetupAlleStufen($db, $conf->entity);
// Nach fehlgeschlagener Validierung die eingegebenen Werte weiterverwenden
foreach ($stufen as $idx => $s) {
if (isset($stufenPost[(int) $s->id])) {
$stufen[$idx] = $stufenPost[(int) $s->id];
}
}
$belegung = mahnungSetupStufenBelegung($db);
// Löschen-Bestätigung (Dolibarr-Dialog). Der 7. Parameter ist $useajax=1, das "Ja"
// ruft deshalb per GET setup.php?rowid=X&action=confirm_delete_stufe&confirm=yes&token=…
// auf — das Token hängt der Dialog selbst an, geprüft wird es oben im CSRF-Wächter.
if ($action === 'ask_delete_stufe' && $rowid > 0 && $user->hasRight('mahnung', 'setup')) {
$askDel = new MahnungStufe($db);
if ($askDel->fetch($rowid) > 0) {
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?rowid='.((int) $askDel->id),
$langs->trans('MahnungStufeLoeschenTitel'),
$langs->transnoentities('MahnungStufeLoeschenFrage', (int) $askDel->stufe, dol_escape_htmltag((string) $askDel->label)),
'confirm_delete_stufe',
'',
0,
1
);
}
}
print '<br><br>';
print load_fiche_titre($langs->trans('MahnungStufen'), '', '');
print '<div class="opacitymedium">'.$langs->trans('MahnungStufenIntro').'</div>';
print '<div class="opacitymedium">'.$langs->trans('MahnungStufenAkkordeonHilfe').'</div><br>';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="save_stufen">';
if (empty($stufen)) {
print '<div class="opacitymedium center">'.$langs->trans('MahnungStufeKeine').'</div>';
}
foreach ($stufen as $s) {
$key = (int) $s->id;
$prefix = 'stufe'.$key.'_';
$nbUse = isset($belegung[(int) $s->stufe]) ? (int) $belegung[(int) $s->stufe] : 0;
// Alle Stufen starten zugeklappt, damit man sie auf einen Blick hat. Ausnahme:
// nach einer fehlgeschlagenen Validierung wird die beanstandete Stufe aufgeklappt
// gezeigt — sonst sucht man die zurückgewiesene Eingabe hinter einer Kopfzeile.
$offen = isset($stufenPost[$key]);
print '<div class="mahnung_stufe_wrap">';
print '<details class="mahnung_stufe"'.($offen ? ' open' : '').'>';
mahnungSetupPrintKopf($s, (string) $key, $nbUse);
print '<div class="mahnung_stufe_body">';
print '<input type="hidden" name="'.$prefix.'present" value="1">';
mahnungSetupPrintFelder($s, $prefix, (string) $key);
print '</div>';
print '</details>';
// Löschen nur, wenn kein Mahnvorgang auf die Stufennummer verweist (Löschsperre;
// serverseitig noch einmal geprüft in der Action confirm_delete_stufe). Der Link
// sitzt bewusst neben dem <summary> und nicht darin: ein Klick auf einen Link
// innerhalb eines <summary> würde zusätzlich den Abschnitt auf- oder zuklappen.
// Ist die Stufe gesperrt, steht der Hinweis dafür in der Kopfzeile.
if ($nbUse === 0) {
print '<span class="mahnung_stufe_aktion">';
print '<a href="'.$_SERVER['PHP_SELF'].'?action=ask_delete_stufe&rowid='.$key.'&token='.newToken().'">';
print img_picto($langs->trans('Delete'), 'delete');
print '</a>';
print '</span>';
}
print '</div>';
}
print '<br><div class="center"><input type="submit" class="button" value="'.$langs->trans('Save').'"></div>';
print '</form>';
// --- Block: Neue Stufe anlegen ----------------------------------------------
$neuVorlage = $neuStufePost;
if ($neuVorlage === null) {
$stufeHelper = new MahnungStufe($db);
$neuVorlage = new MahnungStufe($db);
$neuVorlage->stufe = $stufeHelper->naechsteFreieStufe();
$neuVorlage->label = '';
$neuVorlage->frist_tage = 0;
$neuVorlage->neue_frist_tage = 7;
$neuVorlage->versandart_default = 'pdf';
$neuVorlage->active = 1;
}
print '<br>';
print load_fiche_titre($langs->trans('MahnungStufeNeu'), '', '');
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="add_stufe">';
// Das Anlege-Formular bleibt bewusst offen (kein Akkordeon): es gibt nur eines und
// wer hier landet, will gerade eine Stufe anlegen. Das Aktiv-Häkchen steckt in
// mahnungSetupPrintFelder() — es darf pro Formular nur einmal vorkommen, sonst
// entscheidet die Reihenfolge im POST über den gespeicherten Wert.
print '<div class="mahnung_stufe mahnung_stufe_neu"><div class="mahnung_stufe_body">';
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeNummer'),
'<input type="number" min="0" max="127" name="new_stufe" size="6" value="'.((int) $neuVorlage->stufe).'" required> '
.'<span class="opacitymedium">'.$langs->trans('MahnungStufeNummerHelp').'</span>'
);
mahnungSetupPrintFelder($neuVorlage, 'new_', 'new');
print '</div></div>';
print '<br><div class="center"><input type="submit" class="button" value="'.$langs->trans('MahnungStufeAnlegen').'"></div>';
print '</form>';
// Textbausteine für die Kopfzeilen-Aktualisierung im Browser. transnoentities()
// liefert die Rohfassung ohne HTML-Entities — für JSON ist das richtig, die
// JSON_HEX_*-Flags entschärfen alles, was den <script>-Kontext sprengen könnte.
// Bei "nach %s Tagen" wird als Parameter wieder '%s' übergeben: so kommt die
// Vorlage unverändert zurück (Dolibarr jagt den Text durch sprintf) und das JS
// setzt die Tageszahl selbst ein.
$jsTexte = json_encode(
array(
'kostenlos' => $langs->transnoentities('MahnungStufeKopfKostenlos'),
'sofort' => $langs->transnoentities('MahnungStufeKopfSofort'),
'nachTagen' => $langs->transnoentities('MahnungStufeKopfNachTagen', '%s'),
),
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE
);
// Lokales Umschalt-JS (kein CDN, keine Fremdbibliothek): blendet den kompletten
// KOSTEN-Block aus, sobald eine Stufe als Zahlungserinnerung markiert ist, schaltet
// die E-Mail-Felder nach Versandart um und zieht die zugeklappte Kopfzeile beim
// Tippen live mit. Die Felder werden dabei ausschließlich per display:none versteckt
// und NIE deaktiviert — deaktivierte Felder schickt der Browser nicht mit, und das
// Bulk-Update würde die gespeicherte Gebührenkonfiguration mit Nullen überschreiben.
print '<script>
(function () {
var texte = '.$jsTexte.';
// Sichtbarkeit aller Elemente einer Klasse schalten
function setRows(cls, visible) {
var rows = document.querySelectorAll("." + cls);
for (var i = 0; i < rows.length; i++) {
rows[i].style.display = visible ? "" : "none";
}
}
// Erstes Feld einer Stufe über Klasse + data-mahnungkey finden
function feld(key, cls) {
return document.querySelector("." + cls + "[data-mahnungkey=\'" + key + "\']");
}
function wert(key, cls) {
var el = feld(key, cls);
return el ? el.value : "";
}
// Text setzen — bewusst textContent statt innerHTML, damit eine Bezeichnung mit
// spitzen Klammern nichts in die Kopfzeile einschleusen kann.
function setzeText(cls, key, text) {
var els = document.querySelectorAll("." + cls + "_" + key);
for (var i = 0; i < els.length; i++) {
els[i].textContent = text;
}
}
// Deutsche Betragsformatierung — muss zu mahnungSetupGeld() in PHP passen, sonst
// springt die Kopfzeile beim Tippen auf ein anderes Format als nach dem Neuladen.
// Auch ein negativer Wert wird deshalb angezeigt statt geschluckt: gespeichert
// wird er ohnehin nie, mahnungSetupBetrag() weist ihn mit Meldung zurück.
function mahnungGeld(roh) {
var v = parseFloat(String(roh).replace(",", "."));
if (isNaN(v)) {
v = 0;
}
var vorzeichen = v < 0 ? "-" : "";
var teile = Math.abs(v).toFixed(2).split(".");
teile[0] = teile[0].replace(/\B(?=(\d{3})+(?!\d))/g, ".");
return vorzeichen + teile[0] + "," + teile[1] + " €";
}
function istErinnerung(key) {
var box = document.getElementById("mahnung_erinnerung_" + key);
return !!(box && box.checked);
}
// Sichtbarkeit der E-Mail-Templates: Versandart "E-Mail" oder Zahlungserinnerung
// (die geht immer per E-Mail raus, unabhängig von der eingestellten Versandart).
function syncEmailRows(key) {
var sel = document.getElementById("mahnung_versandart_" + key);
var perMail = !!(sel && sel.value === "mail");
var erinnerung = istErinnerung(key);
setRows("mahnung_emailrow_" + key, perMail || erinnerung);
setRows("mahnung_versandhint_" + key, erinnerung && !perMail);
// Genau umgekehrt: ohne Mahnschreiben braucht es keinen PDF-Einleitungstext.
setRows("mahnung_pdfrow_" + key, !(perMail || erinnerung));
}
// Kompletter KOSTEN-Block weg bei einer Zahlungserinnerung, an seiner Stelle der
// Hinweis, dass die Stufe nichts kostet.
function syncSichtbarkeit(key) {
var an = istErinnerung(key);
setRows("mahnung_kostenblock_" + key, !an);
setRows("mahnung_kostenhint_" + key, an);
syncEmailRows(key);
}
// Zugeklappte Kopfzeile nachziehen, damit sie nie veraltet aussieht
function syncKopf(key) {
var erinnerung = istErinnerung(key);
setzeText("mahnung_kopflabel", key, wert(key, "mahnung_label"));
if (erinnerung) {
setzeText("mahnung_kopfkosten", key, texte.kostenlos);
} else {
var b2c = parseFloat(String(wert(key, "mahnung_gebuehrb2c")).replace(",", "."));
var b2b = parseFloat(String(wert(key, "mahnung_gebuehrb2b")).replace(",", "."));
if (isNaN(b2c)) {
b2c = 0;
}
if (isNaN(b2b)) {
b2b = 0;
}
var txt = mahnungGeld(b2c);
if (Math.abs(b2b - b2c) >= 0.005) {
txt += " / " + mahnungGeld(b2b);
}
setzeText("mahnung_kopfkosten", key, txt);
}
var sel = document.getElementById("mahnung_versandart_" + key);
if (sel && sel.selectedIndex >= 0) {
setzeText("mahnung_kopfversand", key, sel.options[sel.selectedIndex].text);
}
var tage = parseInt(wert(key, "mahnung_frist"), 10);
if (isNaN(tage) || tage < 0) {
tage = 0;
}
setzeText("mahnung_kopffrist", key, tage <= 0 ? texte.sofort : texte.nachTagen.replace("%s", tage));
setRows("mahnung_kopfbadge_" + key, erinnerung);
var akt = feld(key, "mahnung_active");
setRows("mahnung_kopfinaktiv_" + key, !!(akt && !akt.checked));
}
// Auf Ereignisse einer Feldklasse hören und die Kopfzeile aktualisieren
function binde(cls, ereignis) {
var els = document.querySelectorAll("." + cls);
for (var i = 0; i < els.length; i++) {
els[i].addEventListener(ereignis, function () {
syncKopf(this.getAttribute("data-mahnungkey"));
});
}
}
binde("mahnung_label", "input");
binde("mahnung_gebuehrb2c", "input");
binde("mahnung_gebuehrb2b", "input");
binde("mahnung_frist", "input");
binde("mahnung_active", "change");
var selects = document.querySelectorAll("select.mahnung_versandart");
for (var i = 0; i < selects.length; i++) {
selects[i].addEventListener("change", function () {
var key = this.getAttribute("data-mahnungkey");
syncEmailRows(key);
syncKopf(key);
});
}
var boxes = document.querySelectorAll("input.mahnung_erinnerung");
for (var j = 0; j < boxes.length; j++) {
boxes[j].addEventListener("change", function () {
var key = this.getAttribute("data-mahnungkey");
syncSichtbarkeit(key);
syncKopf(key);
});
// Startzustand: nur die Sichtbarkeit angleichen. Die Kopfzeile kommt fertig
// vom Server — sie hier nachzurechnen würde bei einem JS-Fehler eine korrekte
// Anzeige überschreiben.
syncSichtbarkeit(boxes[j].getAttribute("data-mahnungkey"));
}
// Pflichtfeld in einem zugeklappten Abschnitt: die Browser-Meldung kann an einem
// unsichtbaren Feld nicht erscheinen — Chrome bricht das Absenden dann kommentarlos
// ab. Deshalb beim invalid-Ereignis (feuert vor der Meldung, steigt nicht auf,
// daher Capture-Phase) alle übergeordneten <details> aufklappen.
document.addEventListener("invalid", function (e) {
var p = e.target ? e.target.parentNode : null;
while (p && p.nodeType === 1) {
if (p.tagName === "DETAILS") {
p.open = true;
}
p = p.parentNode;
}
}, true);
})();
</script>';
// --- Block: Dokumentenmodelle -----------------------------------------------------------
print '<br><br>';
print load_fiche_titre($langs->trans('MahnungDokumentModelle'), '<a href="templatevars.php" class="button smallpaddingimp">'.$langs->trans('MahnungSetupTemplateVars').'</a>', '');
// Dokumentenmodelle auflisten
$def = array();
$sql = "SELECT nom FROM ".MAIN_DB_PREFIX."document_model WHERE type = 'mahnung' AND entity = ".((int) $conf->entity);
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$def[] = $obj->nom;
}
$db->free($resql);
}
// Verfügbare Modelle scannen (aus den doc/-Klassen)
$dirmodels = array(
DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/mahnung/doc/',
);
$modellist = array();
foreach ($dirmodels as $dmod) {
$files = dol_dir_list($dmod, 'files', 0, '\.modules\.php$');
foreach ($files as $file) {
$classname = preg_replace('/\.modules\.php$/', '', $file['name']);
// Modellname ohne doc_/pdf_-Prefix (commonGenerateDocument fuegt den Prefix selbst hinzu)
$modelname = preg_replace('/^(doc|pdf)_/', '', $classname);
include_once $file['fullname'];
if (class_exists($classname)) {
$obj = new $classname($db);
$modellist[$modelname] = $obj;
}
}
}
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('Name').'</th>';
print '<th>'.$langs->trans('Description').'</th>';
print '<th class="center">'.$langs->trans('Status').'</th>';
print '<th class="center">'.$langs->trans('Default').'</th>';
print '<th></th>';
print '</tr>';
foreach ($modellist as $mname => $mobj) {
print '<tr class="oddeven">';
print '<td>'.dol_escape_htmltag($mobj->name).'</td>';
print '<td>'.dol_escape_htmltag($mobj->description ?? '').'</td>';
// Status (aktiviert/deaktiviert)
print '<td class="center">';
if (in_array($mname, $def)) {
print '<a class="reposition" href="'.$_SERVER['PHP_SELF'].'?action=del&token='.newToken().'&value='.urlencode($mname).'">';
print img_picto($langs->trans('Activated'), 'switch_on');
print '</a>';
} else {
print '<a class="reposition" href="'.$_SERVER['PHP_SELF'].'?action=set&token='.newToken().'&value='.urlencode($mname).'">';
print img_picto($langs->trans('Disabled'), 'switch_off');
print '</a>';
}
print '</td>';
// Default
print '<td class="center">';
if (getDolGlobalString('MAHNUNG_ADDON_PDF') === $mname) {
print img_picto($langs->trans('Default'), 'on');
} elseif (in_array($mname, $def)) {
print '<a class="reposition" href="'.$_SERVER['PHP_SELF'].'?action=setdoc&token='.newToken().'&value='.urlencode($mname).'">';
print img_picto($langs->trans('SetAsDefault'), 'off');
print '</a>';
}
print '</td>';
// Info (ODT: Upload-Formular)
print '<td>';
if (method_exists($mobj, 'info')) {
print $mobj->info($langs);
}
print '</td>';
print '</tr>';
}
print '</table>';
llxFooter();
$db->close();