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>
223 lines
7.5 KiB
PHP
223 lines
7.5 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, version 3.
|
|
*/
|
|
|
|
/**
|
|
* \file htdocs/custom/mahnung/lib/mahnung_anlage.lib.php
|
|
* \ingroup mahnung
|
|
* \brief Gemeinsame Anlage-Logik für Mahnvorgänge.
|
|
*
|
|
* Eingebunden von ajax/createmahnung.php (Einzel-/Massenanlage) und
|
|
* ajax/sammelbrief.php (Anlage + gebündeltes Druck-PDF). Beide Endpunkte bauen
|
|
* ihre Mahnvorgänge aus denselben Vorschlagszeilen. Gebühren, §288-Pauschale,
|
|
* Verzugszinsen, Vorstufenkosten und der Sonderfall "kostenlose
|
|
* Zahlungserinnerung" stehen deshalb genau einmal — hier.
|
|
*/
|
|
|
|
// Nicht direkt per URL aufrufbar — die Datei definiert ausschließlich Funktionen.
|
|
if (!defined('DOL_VERSION')) {
|
|
print 'Diese Datei kann nicht direkt aufgerufen werden.';
|
|
exit;
|
|
}
|
|
|
|
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/class/mahnungvorschlag.class.php';
|
|
|
|
/**
|
|
* Sentinel: der POST-Parameter "stufe" wurde nicht gesetzt.
|
|
* Notwendig, weil 0 (Zahlungserinnerung) eine gültige Stufennummer ist und
|
|
* eine Falsy-Prüfung (`$forceStufe ?: ...`) sie verschlucken würde.
|
|
*/
|
|
if (!defined('MAHNUNG_STUFE_NICHT_GESETZT')) {
|
|
define('MAHNUNG_STUFE_NICHT_GESETZT', -1);
|
|
}
|
|
|
|
/** Sentinel: "stufe" wurde gesetzt, ist aber keine konfigurierte aktive Stufe. */
|
|
if (!defined('MAHNUNG_STUFE_UNGUELTIG')) {
|
|
define('MAHNUNG_STUFE_UNGUELTIG', -2);
|
|
}
|
|
|
|
/**
|
|
* Liest den optionalen POST-Parameter "stufe" (Zielstufe erzwingen).
|
|
*
|
|
* Der Wert wird bewusst als String geholt, damit '' (nicht gesetzt) von '0'
|
|
* (Zahlungserinnerung) unterschieden werden kann. Validiert wird gegen die
|
|
* tatsächlich konfigurierten aktiven Stufen — NICHT gegen einen festen
|
|
* Bereich 1..3, denn Stufennummern sind frei wählbar.
|
|
*
|
|
* @param MahnungVorschlag $service Service, aus dem die aktiven Stufen kommen
|
|
* @return int Stufennummer, MAHNUNG_STUFE_NICHT_GESETZT oder MAHNUNG_STUFE_UNGUELTIG
|
|
*/
|
|
function mahnungGetForceStufe($service)
|
|
{
|
|
$raw = GETPOST('stufe', 'alphanohtml');
|
|
if (is_array($raw)) {
|
|
return MAHNUNG_STUFE_UNGUELTIG;
|
|
}
|
|
$raw = trim((string) $raw);
|
|
if ($raw === '') {
|
|
return MAHNUNG_STUFE_NICHT_GESETZT;
|
|
}
|
|
if (!preg_match('/^-?\d+$/', $raw)) {
|
|
return MAHNUNG_STUFE_UNGUELTIG;
|
|
}
|
|
$nr = (int) $raw;
|
|
if ($service->getStufe($nr) === null) {
|
|
return MAHNUNG_STUFE_UNGUELTIG;
|
|
}
|
|
return $nr;
|
|
}
|
|
|
|
/**
|
|
* Holt die Vorschlagszeilen EINMALIG und indiziert sie nach Rechnungs-ID.
|
|
*
|
|
* getVorschlaege() liest immer die komplette Liste überfälliger Rechnungen —
|
|
* der Aufruf gehört deshalb vor die Schleife, nicht hinein.
|
|
*
|
|
* @param MahnungVorschlag $service
|
|
* @param array $filter Filter für getVorschlaege()
|
|
* @return array|false facture_id => Vorschlagszeile, false bei Fehler
|
|
*/
|
|
function mahnungVorschlagsIndex($service, array $filter = array())
|
|
{
|
|
$rows = $service->getVorschlaege($filter);
|
|
if ($rows === false) {
|
|
return false;
|
|
}
|
|
if (!is_array($rows)) {
|
|
return array();
|
|
}
|
|
|
|
$index = array();
|
|
foreach ($rows as $r) {
|
|
if (!isset($r['facture_id'])) {
|
|
continue;
|
|
}
|
|
$index[(int) $r['facture_id']] = $r;
|
|
}
|
|
return $index;
|
|
}
|
|
|
|
/**
|
|
* Fehlertext des Vorschlag-Service. isset()-Guard, damit die Funktion auch
|
|
* ohne die Property $error keine PHP-Notice auslöst.
|
|
*
|
|
* @param MahnungVorschlag $service
|
|
* @return string
|
|
*/
|
|
function mahnungVorschlagFehlertext($service)
|
|
{
|
|
global $langs;
|
|
|
|
if (isset($service->error) && $service->error !== '') {
|
|
return (string) $service->error;
|
|
}
|
|
return $langs->trans('MahnungVorschlagFehler');
|
|
}
|
|
|
|
/**
|
|
* Baut einen anlagefertigen (noch nicht gespeicherten) Mahnvorgang aus einer
|
|
* Vorschlagszeile und der Zielstufe.
|
|
*
|
|
* Ist die Zielstufe eine Zahlungserinnerung (ist_erinnerung = 1), werden
|
|
* Mahngebühr, §288-Pauschale, Verzugszinsen und Vorstufenkosten ZWINGEND auf 0
|
|
* gesetzt — unabhängig davon, was in der Stufen-Konfiguration steht. Die
|
|
* Erinnerung kostet nichts, Versandart ist immer E-Mail und es entsteht kein
|
|
* Mahn-PDF (pdf_path bleibt leer, Anhang ist die Original-Rechnung).
|
|
*
|
|
* Bei echten Mahnstufen werden die Gebühren/Pauschalen der Vorstufen derselben
|
|
* Rechnung als kosten_vorstufen übernommen und fließen in rechneSumme() ein.
|
|
*
|
|
* @param DoliDB $db
|
|
* @param array $row Zeile aus MahnungVorschlag::getVorschlaege()
|
|
* @param MahnungStufe $stufe Zielstufe
|
|
* @param float $basiszins Basiszinssatz in Prozent
|
|
* @param string $versandart Versandart erzwingen ('' = Default der Stufe)
|
|
* @return Mahnung
|
|
*/
|
|
function mahnungBaueVorgang($db, array $row, $stufe, $basiszins, $versandart = '')
|
|
{
|
|
$factureId = (int) $row['facture_id'];
|
|
$now = dol_now();
|
|
|
|
$mahnung = new Mahnung($db);
|
|
$mahnung->fk_facture = $factureId;
|
|
$mahnung->fk_soc = (int) $row['soc_id'];
|
|
$mahnung->stufe = (int) $stufe->stufe;
|
|
$mahnung->date_mahnung = $now;
|
|
$mahnung->date_lim_reglement_alt = $row['facture_date_lim_reglement'];
|
|
$mahnung->date_lim_reglement_neu = dol_time_plus_duree($now, (int) $stufe->neue_frist_tage, 'd');
|
|
$mahnung->betrag_offen = (float) $row['betrag_offen'];
|
|
$mahnung->customertype = $row['kundentyp'];
|
|
$mahnung->basiszins_snapshot = $basiszins;
|
|
|
|
if ($stufe->istErinnerung()) {
|
|
// Kostenlose Zahlungserinnerung: keine Mahngebühr, keine Pauschale nach
|
|
// §288 Abs. 5, keine Verzugszinsen, keine Vorstufenkosten. Bewusst hart
|
|
// genullt und nicht aus der Stufen-Konfiguration übernommen.
|
|
$mahnung->mahngebuehr = 0;
|
|
$mahnung->pauschale_b2b = 0;
|
|
$mahnung->verzugszinsen = 0;
|
|
$mahnung->kosten_vorstufen = 0;
|
|
$mahnung->versandart = Mahnung::VERSAND_MAIL;
|
|
$mahnung->pdf_path = null;
|
|
} else {
|
|
$mahnung->versandart = $versandart ?: ($stufe->versandart_default ?: Mahnung::VERSAND_PDF);
|
|
$mahnung->mahngebuehr = $stufe->getMahngebuehr($mahnung->customertype);
|
|
|
|
// §288 Abs. 5 Pauschale: nur einmal pro Rechnung und nur B2B
|
|
if ($mahnung->customertype === Mahnung::KUNDENTYP_B2B
|
|
&& (int) $stufe->pauschale_b2b_einmalig === 1
|
|
&& !mahnungPauschaleBereitsAngewendet($db, $factureId)) {
|
|
$mahnung->pauschale_b2b = (float) getDolGlobalString('MAHNUNG_PAUSCHALE_B2B', '40.00');
|
|
}
|
|
|
|
$mahnung->verzugszinsen = Mahnung::berechneVerzugszinsen(
|
|
$mahnung->betrag_offen,
|
|
(int) $row['tage_verzug'],
|
|
$mahnung->customertype,
|
|
$basiszins,
|
|
$stufe->getZinssatzOverride($mahnung->customertype)
|
|
);
|
|
|
|
// Gebühren/Pauschalen der Vorstufen kumulieren (Zinsen bleiben außen vor,
|
|
// die werden je Stufe tagesgenau neu gerechnet).
|
|
$mahnung->kosten_vorstufen = $mahnung->summeVorstufenKosten($factureId);
|
|
}
|
|
|
|
$mahnung->rechneSumme();
|
|
$mahnung->status = Mahnung::STATUS_ERSTELLT;
|
|
|
|
return $mahnung;
|
|
}
|
|
|
|
/**
|
|
* Prüft, ob für eine Rechnung bereits in einer nicht stornierten Mahnung die
|
|
* §288-Abs.-5-Pauschale berechnet wurde.
|
|
*
|
|
* @param DoliDB $db
|
|
* @param int $factureId
|
|
* @return bool
|
|
*/
|
|
function mahnungPauschaleBereitsAngewendet($db, $factureId)
|
|
{
|
|
$sql = "SELECT 1 FROM ".MAIN_DB_PREFIX."mahnung_mahnung";
|
|
$sql .= " WHERE fk_facture = ".((int) $factureId);
|
|
$sql .= " AND entity IN (".getEntity('mahnung').")";
|
|
$sql .= " AND status <> ".((int) Mahnung::STATUS_STORNIERT);
|
|
$sql .= " AND pauschale_b2b > 0";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if (!$resql) {
|
|
dol_syslog('mahnungPauschaleBereitsAngewendet SQL-Fehler: '.$db->lasterror(), LOG_ERR);
|
|
return false;
|
|
}
|
|
$has = (bool) $db->num_rows($resql);
|
|
$db->free($resql);
|
|
return $has;
|
|
}
|