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>
198 lines
6.7 KiB
PHP
198 lines
6.7 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 core/triggers/interface_99_modMahnung_MahnungTriggers.class.php
|
|
* \ingroup mahnung
|
|
* \brief Trigger: setzt offene Mahnvorgänge nach Zahlungseingang auf "erledigt".
|
|
*
|
|
* Klassenname: Interface<Suffix>Triggers — Suffix muss zu Dateinamen passen.
|
|
* Datei muss in core/triggers/ liegen, wird durch DolibarrModules->loadtriggers
|
|
* (module_parts['triggers']=1) beim Modul-Aktivieren registriert.
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/triggers/dolibarrtriggers.class.php';
|
|
|
|
class InterfaceMahnungTriggers extends DolibarrTriggers
|
|
{
|
|
/**
|
|
* @param DoliDB $db Datenbank-Handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
parent::__construct($db);
|
|
$this->family = 'financial';
|
|
$this->description = 'MahnungTriggerBeschreibung';
|
|
$this->version = self::VERSIONS['dev'];
|
|
$this->picto = 'fa-envelope-open-text';
|
|
}
|
|
|
|
/**
|
|
* Wird bei jedem Dolibarr-Event aufgerufen.
|
|
*
|
|
* @param string $action z.B. PAYMENT_CUSTOMER_CREATE, BILL_PAYED
|
|
* @param CommonObject $object Event-Subjekt
|
|
* @param User $user aktueller User
|
|
* @param Translate $langs Sprache
|
|
* @param Conf $conf Konfig
|
|
* @return int 0 nichts getan, >0 OK — bewusst NIE <0, weil
|
|
* Dolibarr sonst die auslösende Transaktion
|
|
* (z.B. die Zahlungsbuchung) zurückrollt.
|
|
*/
|
|
public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf)
|
|
{
|
|
if (!isModEnabled('mahnung')) {
|
|
return 0;
|
|
}
|
|
|
|
switch ($action) {
|
|
case 'BILL_PAYED':
|
|
case 'BILL_VALIDATE':
|
|
// $object ist Facture
|
|
if (!empty($object) && !empty($object->id) && !empty($object->paye)) {
|
|
return $this->onRechnungBezahlt((int) $object->id, $user);
|
|
}
|
|
return 0;
|
|
|
|
case 'PAYMENT_CUSTOMER_CREATE':
|
|
// $object ist Paiement; eventuell auch ohne paye=1 Status — pro betroffener Rechnung prüfen
|
|
return $this->onPaiementErzeugt($object, $user);
|
|
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setzt alle offenen Mahnvorgänge zur Rechnung auf STATUS_ERLEDIGT.
|
|
*
|
|
* WICHTIG: Dieser Trigger darf NIEMALS einen negativen Wert liefern. Dolibarr
|
|
* rollt bei <0 die auslösende Transaktion zurück — eine kaputte oder noch nicht
|
|
* migrierte Mahnungs-Tabelle würde damit die Zahlungsbuchung des Users
|
|
* verhindern. Fehler werden deshalb nur geloggt, Rückgabe bleibt 0.
|
|
*
|
|
* @param int $factureId
|
|
* @param User $user
|
|
* @return int >=0 Anzahl aktualisierter Mahnungen (0 auch im Fehlerfall)
|
|
*/
|
|
private function onRechnungBezahlt($factureId, User $user)
|
|
{
|
|
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php';
|
|
|
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."mahnung_mahnung";
|
|
$sql .= " WHERE fk_facture = ".((int) $factureId);
|
|
$sql .= " AND entity IN (".getEntity('mahnung').")";
|
|
$sql .= " AND status NOT IN (".Mahnung::STATUS_ERLEDIGT.", ".Mahnung::STATUS_STORNIERT.")";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
// Kein -1: das würde die Zahlungsbuchung des Users zurückrollen
|
|
dol_syslog('InterfaceMahnungTriggers::onRechnungBezahlt SQL-Fehler: '.$this->db->lasterror(), LOG_ERR);
|
|
return 0;
|
|
}
|
|
|
|
$count = 0;
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$m = new Mahnung($this->db);
|
|
if ($m->fetch((int) $obj->rowid) > 0 && $m->setErledigt($user) > 0) {
|
|
$count++;
|
|
} else {
|
|
// Nur protokollieren — ein einzelner Fehlschlag darf die Buchung nicht kippen
|
|
dol_syslog('InterfaceMahnungTriggers::onRechnungBezahlt Mahnung '.((int) $obj->rowid).' nicht erledigt: '.$m->error, LOG_WARNING);
|
|
}
|
|
}
|
|
$this->db->free($resql);
|
|
|
|
// Wenn Mahnungen erledigt wurden: prüfen ob noch offene übrig sind.
|
|
// Falls nicht, GlobalNotify-Badge räumen.
|
|
if ($count > 0 && !$this->hatOffeneMahnungen()) {
|
|
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungcron.class.php';
|
|
MahnungCron::clearGlobalNotify();
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Bei Zahlungs-Erzeugung: alle betroffenen Rechnungen prüfen, ob sie nun
|
|
* vollständig bezahlt sind. paye-Flag wird in Dolibarr typischerweise erst
|
|
* gesetzt, wenn Summe der Zahlungen >= total_ttc — wir prüfen pessimistisch:
|
|
* sobald überhaupt eine Zahlung kommt + offene Mahnung existiert -> erledigen,
|
|
* wenn die Zahlung den offenen Betrag deckt.
|
|
*
|
|
* @param CommonObject $payment
|
|
* @param User $user
|
|
* @return int
|
|
*/
|
|
private function onPaiementErzeugt($payment, User $user)
|
|
{
|
|
if (empty($payment) || empty($payment->amounts) || !is_array($payment->amounts)) {
|
|
return 0;
|
|
}
|
|
$total = 0;
|
|
foreach ($payment->amounts as $factureId => $amount) {
|
|
$factureId = (int) $factureId;
|
|
if ($factureId <= 0) {
|
|
continue;
|
|
}
|
|
// Prüfen, ob die Rechnung jetzt voll bezahlt ist
|
|
if ($this->istRechnungVollBezahlt($factureId)) {
|
|
$total += $this->onRechnungBezahlt($factureId, $user);
|
|
}
|
|
}
|
|
return $total;
|
|
}
|
|
|
|
/**
|
|
* Prüft ob noch offene Mahnvorgänge existieren (Status weder erledigt noch storniert).
|
|
*
|
|
* @return bool true wenn mindestens eine offene Mahnung existiert
|
|
*/
|
|
private function hatOffeneMahnungen()
|
|
{
|
|
$sql = "SELECT COUNT(*) AS nb FROM ".MAIN_DB_PREFIX."mahnung_mahnung";
|
|
$sql .= " WHERE entity IN (".getEntity('mahnung').")";
|
|
$sql .= " AND status NOT IN (".Mahnung::STATUS_ERLEDIGT.", ".Mahnung::STATUS_STORNIERT.")";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
dol_syslog('InterfaceMahnungTriggers::hatOffeneMahnungen SQL-Fehler: '.$this->db->lasterror(), LOG_ERR);
|
|
return true; // Im Zweifel Badge stehen lassen
|
|
}
|
|
$obj = $this->db->fetch_object($resql);
|
|
$this->db->free($resql);
|
|
return (int) $obj->nb > 0;
|
|
}
|
|
|
|
/**
|
|
* @param int $factureId
|
|
* @return bool
|
|
*/
|
|
private function istRechnungVollBezahlt($factureId)
|
|
{
|
|
$sql = "SELECT f.total_ttc, COALESCE(SUM(pf.amount), 0) AS gezahlt";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."paiement_facture as pf ON pf.fk_facture = f.rowid";
|
|
$sql .= " WHERE f.rowid = ".((int) $factureId);
|
|
$sql .= " GROUP BY f.rowid, f.total_ttc";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
dol_syslog('InterfaceMahnungTriggers::istRechnungVollBezahlt SQL-Fehler: '.$this->db->lasterror(), LOG_ERR);
|
|
return false;
|
|
}
|
|
if (!$this->db->num_rows($resql)) {
|
|
$this->db->free($resql);
|
|
return false;
|
|
}
|
|
$obj = $this->db->fetch_object($resql);
|
|
$this->db->free($resql);
|
|
return (float) $obj->gezahlt + 0.005 >= (float) $obj->total_ttc;
|
|
}
|
|
}
|