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