*
* 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
//
// 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
: die Stufen stecken in -Blöcken, und HTML erlaubt kein
* 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 '
';
print '
'.$label.'
';
print '
'.$inputHtml.'
';
print '
';
}
/**
* Zwischenüberschrift eines Blocks (FRISTEN / KOSTEN / VERSAND).
*
* @param string $text Bereits übersetzte Überschrift
* @return void
*/
function mahnungSetupBlockTitel($text)
{
print '
'.$text.'
';
}
/**
* Die zugeklappte Kopfzeile einer Mahnstufe ausgeben ( 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 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 '';
print ''.((int) $s->stufe).'';
print ''.dol_escape_htmltag((string) $s->label).'';
// Abzeichen "Zahlungserinnerung" — immer gerendert, per JS ein-/ausgeblendet
print '';
print $langs->trans('MahnungStufeIstErinnerung').'';
// Abzeichen "inaktiv" — ebenfalls immer gerendert, damit das Aktiv-Häkchen im
// aufgeklappten Zustand die Kopfzeile ohne Neuladen mitziehen kann
print 'active) ? '' : ' style="display:none;"').'>';
print $langs->trans('MahnungStufeInaktiv').'';
// 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 '';
print $langs->trans('MahnungStufeNichtLoeschbarKurz').'';
}
print '';
print '';
print $istErinnerung ? $langs->trans('MahnungStufeKopfKostenlos') : dol_escape_htmltag(mahnungSetupKopfGebuehr($s));
print ' · ';
print ''.$vaText.' · ';
print ''.mahnungSetupKopfFrist($s->frist_tage).'';
print '';
print '';
}
/**
* 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 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'),
''
);
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeIstErinnerung'),
' '
.''.$langs->trans('MahnungStufeIstErinnerungHelp').''
);
mahnungSetupFeldZeile(
$langs->trans('Active'),
'active) ? ' checked' : '').'>'
);
// --- Block FRISTEN ----------------------------------------------------------
mahnungSetupBlockTitel($langs->trans('MahnungBlockFristen'));
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeFristTage'),
''
);
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeNeueFristTage'),
''
);
// --- Block KOSTEN (bei einer Zahlungserinnerung komplett ausgeblendet) -------
print '
';
print '';
mahnungSetupBlockTitel($langs->trans('MahnungBlockKosten'));
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeGebuehrB2C'),
' EUR'
);
mahnungSetupFeldZeile(
$langs->trans('MahnungStufeGebuehrB2B'),
' EUR'
);
mahnungSetupFeldZeile(
$langs->trans('MahnungPauschaleB2B').' (§288 Abs. 5)',
'pauschale_b2b_einmalig) ? ' checked' : '').'>'
);
// Zinssatz-Overrides: selten gebraucht, deshalb hinter einem eigenen Abschnitt.
// Ist bereits ein Override gesetzt, startet er aufgeklappt.
print '';
print ''.$langs->trans('MahnungBlockZinssatzOverride').'';
print '
';
// --- Block VERSAND ----------------------------------------------------------
mahnungSetupBlockTitel($langs->trans('MahnungBlockVersand'));
$versandHtml = '';
// 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 .= ' ';
$versandHtml .= $langs->trans('MahnungStufeErinnerungVersandHinweis').'';
mahnungSetupFeldZeile($langs->trans('MahnungStufeVersandartDefault'), $versandHtml);
// Textbausteine: ebenfalls selten angefasst, deshalb zugeklappt. Ist bereits ein
// Text hinterlegt, startet der Abschnitt offen.
print '';
print ''.$langs->trans('MahnungBlockTexte').'';
print '
';
mahnungSetupFeldZeile(
$langs->trans('MahnungStufePdfIntro'),
'',
'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(
'',
''.$langs->trans('MahnungPlatzhalterHilfe').'',
'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'),
'',
'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 '
';
print '';
}
// ---------------------------------------------------------------
// 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 = ''.dol_escape_htmltag($langs->trans('MahnungTrackingPatternsSetup')).'';
print load_fiche_titre($langs->trans('MahnungSetupPage'), $setupExtra, 'fa-envelope-open-text');
print ''.$langs->trans('MahnungSetupDescription').'
';
// --- Block: Konstanten -------------------------------------------------------
print '';
// --- 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 '';
$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 '