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>
1467 lines
63 KiB
PHP
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();
|