Feature: ODT-Template-System, Widget, Dokumentenliste, Templatevariablen-Referenz [deploy]
All checks were successful
Deploy mahnung / deploy (push) Successful in 13s

- ODT-Template-System: Pro Mahnstufe eigenes Template (mahnung_stufe1/2/3.odt),
  Fallback auf generisches Template; Basis-Klasse ModelePDFMahnung,
  pdf_standard_mahnung (TCPDF refactored), doc_generic_mahnung_odt (ODTPHP)
- Widget box_mahnung_offen: Überfällige Rechnungen mit Mahnstufe-Badge (blau/orange/rot)
- card.php: Dokumentenliste mit Lupe (PDF-Vorschau), Download, Modellauswahl
- admin/templatevars.php: Referenzseite aller verfügbaren ODT-Variablen
- admin/setup.php: Dokumentenmodell-Verwaltung, Upload-Bereich mit Benennungskonvention
- mahnung.class.php: generateDocument() + socid-Alias für commonGenerateDocument()
- modMahnung.class.php: models=1, MAHNUNG_ADDON_PDF/ODT_PATH-Konstanten,
  document_model-Registrierung in init(), Widget registriert
- mahnungpdf.class.php entfernt, Logik in pdf_standard_mahnung.modules.php

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-05-10 18:28:24 +02:00
parent 1af917d818
commit 993ac50c0c
11 changed files with 1432 additions and 120 deletions

View file

@ -42,8 +42,10 @@ if (!$res) {
}
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/mahnung/modules_mahnung.php';
global $langs, $user, $conf, $db;
$langs->loadLangs(array('admin', 'mahnung@mahnung'));
@ -165,6 +167,35 @@ function loadStufeById($db, $id, $entity)
return $s;
}
// ---------------------------------------------------------------
// Dokumentenmodell-Aktionen (vor View, da header()-Redirect)
// ---------------------------------------------------------------
if ($action === 'setdoc' && $user->hasRight('mahnung', 'setup')) {
$newModel = GETPOST('value', 'alphanohtml');
if (!empty($newModel)) {
dolibarr_set_const($db, 'MAHNUNG_ADDON_PDF', $newModel, 'chaine', 0, '', $conf->entity);
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
if ($action === 'set' && $user->hasRight('mahnung', 'setup')) {
$newModel = GETPOST('value', 'alphanohtml');
$sql = "INSERT INTO ".MAIN_DB_PREFIX."document_model (nom, type, entity, libelle) VALUES ('".$db->escape($newModel)."', 'mahnung', ".((int) $conf->entity).", '".$db->escape($newModel)."')";
$db->query($sql);
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
if ($action === 'del' && $user->hasRight('mahnung', 'setup')) {
$newModel = GETPOST('value', 'alphanohtml');
$sql = "DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = '".$db->escape($newModel)."' AND type = 'mahnung' AND entity = ".((int) $conf->entity);
$db->query($sql);
if (getDolGlobalString('MAHNUNG_ADDON_PDF') === $newModel) {
dolibarr_del_const($db, 'MAHNUNG_ADDON_PDF', $conf->entity);
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
// ---------------------------------------------------------------
// View
// ---------------------------------------------------------------
@ -278,5 +309,91 @@ print '</table>';
print '<br><div class="center"><input type="submit" class="button" value="'.$langs->trans('Save').'"></div>';
print '</form>';
// --- Block: Dokumentenmodelle -----------------------------------------------------------
print '<br><br>';
print load_fiche_titre($langs->trans('MahnungDokumentModelle'), '<a href="templatevars.php" class="button smallpaddingimp">Verfuegbare Template-Variablen</a>', '');
// Aktionen fuer Dokumentenmodelle (Upload, Loeschen, setModuleOptions)
include_once DOL_DOCUMENT_ROOT.'/core/actions_setmoduleoptions.inc.php';
// Dokumentenmodelle auflisten
$def = array();
$sql = "SELECT nom FROM ".MAIN_DB_PREFIX."document_model WHERE type = 'mahnung' AND entity = ".((int) $conf->entity);
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$def[] = $obj->nom;
}
$db->free($resql);
}
// Verfuegbare Modelle scannen (aus den doc/-Klassen)
$dirmodels = array(
DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/mahnung/doc/',
);
$modellist = array();
foreach ($dirmodels as $dmod) {
$files = dol_dir_list($dmod, 'files', 0, '\.modules\.php$');
foreach ($files as $file) {
$classname = preg_replace('/\.modules\.php$/', '', $file['name']);
// Modellname ohne doc_/pdf_-Prefix (commonGenerateDocument fuegt den Prefix selbst hinzu)
$modelname = preg_replace('/^(doc|pdf)_/', '', $classname);
include_once $file['fullname'];
if (class_exists($classname)) {
$obj = new $classname($db);
$modellist[$modelname] = $obj;
}
}
}
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('Name').'</th>';
print '<th>'.$langs->trans('Description').'</th>';
print '<th class="center">'.$langs->trans('Status').'</th>';
print '<th class="center">'.$langs->trans('Default').'</th>';
print '<th></th>';
print '</tr>';
foreach ($modellist as $mname => $mobj) {
print '<tr class="oddeven">';
print '<td>'.dol_escape_htmltag($mobj->name).'</td>';
print '<td>'.dol_escape_htmltag($mobj->description ?? '').'</td>';
// Status (aktiviert/deaktiviert)
print '<td class="center">';
if (in_array($mname, $def)) {
print '<a class="reposition" href="'.$_SERVER['PHP_SELF'].'?action=del&token='.newToken().'&value='.urlencode($mname).'">';
print img_picto($langs->trans('Activated'), 'switch_on');
print '</a>';
} else {
print '<a class="reposition" href="'.$_SERVER['PHP_SELF'].'?action=set&token='.newToken().'&value='.urlencode($mname).'">';
print img_picto($langs->trans('Disabled'), 'switch_off');
print '</a>';
}
print '</td>';
// Default
print '<td class="center">';
if (getDolGlobalString('MAHNUNG_ADDON_PDF') === $mname) {
print img_picto($langs->trans('Default'), 'on');
} elseif (in_array($mname, $def)) {
print '<a class="reposition" href="'.$_SERVER['PHP_SELF'].'?action=setdoc&token='.newToken().'&value='.urlencode($mname).'">';
print img_picto($langs->trans('SetAsDefault'), 'off');
print '</a>';
}
print '</td>';
// Info (ODT: Upload-Formular)
print '<td>';
if (method_exists($mobj, 'info')) {
print $mobj->info($langs);
}
print '</td>';
print '</tr>';
}
print '</table>';
llxFooter();
$db->close();

220
admin/templatevars.php Normal file
View file

@ -0,0 +1,220 @@
<?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 mahnung/admin/templatevars.php
* \ingroup mahnung
* \brief Uebersicht aller verfuegbaren Variablen fuer ODT-Templates.
*/
$res = 0;
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
}
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
$tmp2 = realpath(__FILE__);
$i = strlen($tmp) - 1;
$j = strlen($tmp2) - 1;
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
$i--;
$j--;
}
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
}
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
}
if (!$res && file_exists("../../main.inc.php")) {
$res = @include "../../main.inc.php";
}
if (!$res) {
die("Include of main fails");
}
global $langs, $user;
$langs->loadLangs(array('admin', 'mahnung@mahnung'));
if (!$user->admin && !$user->hasRight('mahnung', 'setup')) {
accessforbidden();
}
llxHeader('', 'Mahnung — Template-Variablen');
print load_fiche_titre('Verfuegbare Variablen fuer ODT-Templates', '<a href="setup.php">Zurueck zum Setup</a>', 'fa-envelope-open-text');
print '<div class="opacitymedium" style="margin-bottom: 15px;">';
print 'Diese Variablen koennen in ODT-Templates mit geschweiften Klammern verwendet werden, z.B. <code>{mahnung_ref}</code>.<br>';
print 'Stufen-spezifische Templates: <code>mahnung_stufe1.odt</code>, <code>mahnung_stufe2.odt</code>, <code>mahnung_stufe3.odt</code> — Fallback: beliebiges <code>.odt</code> im Template-Verzeichnis.';
print '</div>';
// Mahnung-Variablen
print '<table class="noborder centpercent">';
print '<tr class="liste_titre"><th colspan="3">Mahnung</th></tr>';
print '<tr class="liste_titre"><th>Variable</th><th>Beschreibung</th><th>Beispiel</th></tr>';
$mahnungVars = array(
array('{mahnung_ref}', 'Mahnung-Referenznummer', 'MAHN2026-0001'),
array('{mahnung_stufe}', 'Mahnstufe (Nummer)', '1'),
array('{mahnung_stufe_label}', 'Bezeichnung der Mahnstufe', 'Zahlungserinnerung'),
array('{mahnung_date}', 'Datum der Mahnung', '10.05.2026'),
array('{mahnung_date_lim_alt}', 'Urspruengliches Faelligkeitsdatum der Rechnung', '25.04.2026'),
array('{mahnung_date_lim_neu}', 'Neue Zahlungsfrist', '24.05.2026'),
array('{mahnung_betrag_offen}', 'Offener Rechnungsbetrag', '131,34'),
array('{mahnung_mahngebuehr}', 'Mahngebuehr dieser Stufe', '5,00'),
array('{mahnung_pauschale_b2b}', 'B2B-Pauschale nach BGB §288 Abs. 5', '40,00'),
array('{mahnung_verzugszinsen}', 'Berechnete Verzugszinsen', '1,23'),
array('{mahnung_summe}', 'Gesamtforderung (offen + Gebuehren + Zinsen)', '177,57'),
array('{mahnung_basiszins}', 'BGB-Basiszinssatz (Snapshot bei Erstellung)', '1,27'),
array('{mahnung_zinssatz}', 'Effektiver Zinssatz (Basis + Aufschlag)', '6,27'),
array('{mahnung_kundentyp}', 'Kundentyp', 'B2C oder B2B'),
array('{mahnung_versandart}', 'Versandart', 'pdf, mail, druck, none'),
array('{mahnung_pdf_intro}', 'Einleitungstext der Mahnstufe (aus Setup oder Default)', 'unsere unten aufgefuehrte Rechnung...'),
);
foreach ($mahnungVars as $v) {
print '<tr class="oddeven">';
print '<td><code>'.dol_escape_htmltag($v[0]).'</code></td>';
print '<td>'.dol_escape_htmltag($v[1]).'</td>';
print '<td class="opacitymedium">'.dol_escape_htmltag($v[2]).'</td>';
print '</tr>';
}
print '</table>';
// Rechnungs-Variablen
print '<br>';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre"><th colspan="3">Verknuepfte Rechnung</th></tr>';
print '<tr class="liste_titre"><th>Variable</th><th>Beschreibung</th><th>Beispiel</th></tr>';
$factureVars = array(
array('{facture_ref}', 'Rechnungsnummer', 'IN2604-0036'),
array('{facture_date}', 'Rechnungsdatum', '01.04.2026'),
array('{facture_date_lim}', 'Original-Faelligkeitsdatum', '25.04.2026'),
array('{facture_total_ht}', 'Nettobetrag der Rechnung', '110,37'),
array('{facture_total_ttc}', 'Bruttobetrag der Rechnung', '131,34'),
array('{facture_total_tva}', 'MwSt-Betrag', '20,97'),
array('{facture_already_paid}', 'Bereits gezahlter Betrag', '0,00'),
);
foreach ($factureVars as $v) {
print '<tr class="oddeven">';
print '<td><code>'.dol_escape_htmltag($v[0]).'</code></td>';
print '<td>'.dol_escape_htmltag($v[1]).'</td>';
print '<td class="opacitymedium">'.dol_escape_htmltag($v[2]).'</td>';
print '</tr>';
}
print '</table>';
// Firmen-Variablen (Absender)
print '<br>';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre"><th colspan="3">Eigene Firma (Absender)</th></tr>';
print '<tr class="liste_titre"><th>Variable</th><th>Beschreibung</th><th>Beispiel</th></tr>';
$mysocVars = array(
array('{mycompany_name}', 'Firmenname', 'Alles Watt Laeuft'),
array('{mycompany_address}', 'Strasse', 'Musterstrasse 1'),
array('{mycompany_zip}', 'PLZ', '24536'),
array('{mycompany_town}', 'Ort', 'Neumuenster'),
array('{mycompany_country}', 'Land', 'Deutschland'),
array('{mycompany_phone}', 'Telefonnummer', '04321 1234567'),
array('{mycompany_fax}', 'Faxnummer', ''),
array('{mycompany_email}', 'E-Mail-Adresse', 'info@example.de'),
array('{mycompany_web}', 'Webseite', 'www.example.de'),
array('{mycompany_idprof1}', 'Handelsregisternummer', ''),
array('{mycompany_idprof2}', 'SIRET/Steuernummer', ''),
array('{mycompany_capital}', 'Stammkapital', ''),
array('{mycompany_logo}', 'Firmenlogo (wird als Bild eingefuegt)', '(Bilddatei)'),
);
foreach ($mysocVars as $v) {
print '<tr class="oddeven">';
print '<td><code>'.dol_escape_htmltag($v[0]).'</code></td>';
print '<td>'.dol_escape_htmltag($v[1]).'</td>';
print '<td class="opacitymedium">'.dol_escape_htmltag($v[2]).'</td>';
print '</tr>';
}
print '</table>';
// Kunden-Variablen
print '<br>';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre"><th colspan="3">Kunde (Empfaenger)</th></tr>';
print '<tr class="liste_titre"><th>Variable</th><th>Beschreibung</th><th>Beispiel</th></tr>';
$companyVars = array(
array('{company_name}', 'Kundenname', 'Brigitte Ladewig'),
array('{company_alias}', 'Kurzname/Alias', ''),
array('{company_address}', 'Strasse', 'Beispielweg 5'),
array('{company_zip}', 'PLZ', '24534'),
array('{company_town}', 'Ort', 'Neumuenster'),
array('{company_country}', 'Land', 'Deutschland'),
array('{company_phone}', 'Telefon', '04321 9876543'),
array('{company_email}', 'E-Mail', 'b.ladewig@example.de'),
array('{company_idprof1}', 'Handelsregister', ''),
array('{company_idprof2}', 'Steuernummer', ''),
array('{company_vatnumber}', 'USt-IdNr.', 'DE123456789'),
array('{company_note_public}', 'Oeffentliche Notiz des Kunden', ''),
);
foreach ($companyVars as $v) {
print '<tr class="oddeven">';
print '<td><code>'.dol_escape_htmltag($v[0]).'</code></td>';
print '<td>'.dol_escape_htmltag($v[1]).'</td>';
print '<td class="opacitymedium">'.dol_escape_htmltag($v[2]).'</td>';
print '</tr>';
}
print '</table>';
// Bank-Variablen
print '<br>';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre"><th colspan="3">Bankverbindung</th></tr>';
print '<tr class="liste_titre"><th>Variable</th><th>Beschreibung</th><th>Beispiel</th></tr>';
$bankVars = array(
array('{mahnung_bank_label}', 'Name der Bank', 'Sparkasse Suedholstein'),
array('{mahnung_bank_iban}', 'IBAN', 'DE89 3704 0044 0532 0130 00'),
array('{mahnung_bank_bic}', 'BIC/SWIFT', 'COBADEFFXXX'),
);
foreach ($bankVars as $v) {
print '<tr class="oddeven">';
print '<td><code>'.dol_escape_htmltag($v[0]).'</code></td>';
print '<td>'.dol_escape_htmltag($v[1]).'</td>';
print '<td class="opacitymedium">'.dol_escape_htmltag($v[2]).'</td>';
print '</tr>';
}
print '</table>';
// Dolibarr-Standard-Variablen
print '<br>';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre"><th colspan="3">Dolibarr-Standard (Auswahl)</th></tr>';
print '<tr class="liste_titre"><th>Variable</th><th>Beschreibung</th><th>Beispiel</th></tr>';
$stdVars = array(
array('{__FROM_NAME__}', 'Absender-Name', 'Alles Watt Laeuft'),
array('{__FROM_EMAIL__}', 'Absender-E-Mail', 'info@example.de'),
array('{__DATE__}', 'Aktuelles Datum', '10.05.2026'),
array('{myuser_lastname}', 'Nachname des eingeloggten Users', 'Wisch'),
array('{myuser_firstname}', 'Vorname des eingeloggten Users', 'Eduard'),
array('{myuser_email}', 'E-Mail des eingeloggten Users', 'data@example.de'),
);
foreach ($stdVars as $v) {
print '<tr class="oddeven">';
print '<td><code>'.dol_escape_htmltag($v[0]).'</code></td>';
print '<td>'.dol_escape_htmltag($v[1]).'</td>';
print '<td class="opacitymedium">'.dol_escape_htmltag($v[2]).'</td>';
print '</tr>';
}
print '</table>';
llxFooter();
$db->close();

View file

@ -28,7 +28,6 @@ require_once $_SERVER['DOCUMENT_ROOT'].'/main.inc.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungvorschlag.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungpdf.class.php';
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
@ -101,7 +100,6 @@ $forceStufe = ($forceStufe >= 1 && $forceStufe <= 3) ? $forceStufe : 0;
// 4) Verarbeitung — pro Rechnung Vorschlag holen, Mahnung erzeugen, PDF generieren
$service = new MahnungVorschlag($db);
$pdfGen = new MahnungPdf($db);
$basiszins = (float) getDolGlobalString('MAHNUNG_BASISZINS', '1.27');
$created = 0;
@ -172,9 +170,9 @@ foreach ($factureIds as $fid) {
continue;
}
$pdfPath = $pdfGen->generate($mahnung, $user);
if ($pdfPath === false) {
$failed[] = 'Rechnung #'.$fid.' (Mahnung '.$mahnung->ref.'): PDF-Fehler '.$pdfGen->error;
$docResult = $mahnung->generateDocument('', $langs);
if ($docResult <= 0) {
$failed[] = 'Rechnung #'.$fid.' (Mahnung '.$mahnung->ref.'): Dokument-Fehler '.$mahnung->error;
continue;
}

134
card.php
View file

@ -57,17 +57,26 @@ if ($mahnung->fetch($id) <= 0) {
// Stornieren
if ($action === 'storno' && $user->hasRight('mahnung', 'delete')) {
if (!verifCsrf($_POST['token'] ?? '', 'mahnung_storno')) {
setEventMessages($langs->trans('ErrorBadValueForToken'), null, 'errors');
} else {
$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');
$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' && $user->hasRight('mahnung', 'write')) {
$selectedModel = GETPOST('model', 'alphanohtml');
$result = $mahnung->generateDocument($selectedModel, $langs);
if ($result > 0) {
setEventMessages('Dokument erstellt', null, 'mesgs');
} else {
setEventMessages('Dokument-Fehler: '.$mahnung->error, null, 'errors');
}
header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id));
exit;
}
llxHeader('', $langs->trans('MahnungRef').' '.$mahnung->ref);
@ -103,18 +112,80 @@ if ((float) $mahnung->pauschale_b2b > 0) {
print '<tr><td>'.$langs->trans('MahnungVerzugszinsen').'</td><td>'.price($mahnung->verzugszinsen).' (Basiszins '.number_format((float) $mahnung->basiszins_snapshot, 2, ',', '.').' %)</td></tr>';
print '<tr><td>'.$langs->trans('MahnungSumme').'</td><td><strong>'.price($mahnung->summe_mahnung).'</strong></td></tr>';
print '<tr><td>Status</td><td>'.dol_escape_htmltag($mahnung->getStatusLabel()).'</td></tr>';
if (!empty($mahnung->pdf_path)) {
$relativePdf = str_replace(DOL_DATA_ROOT, '', $mahnung->pdf_path);
$dl = DOL_URL_ROOT.'/document.php?modulepart=facture&file='.urlencode(ltrim(str_replace('/facture/', '', $relativePdf), '/'));
print '<tr><td>PDF</td><td><a href="'.$dl.'">PDF herunterladen</a></td></tr>';
}
print '</table>';
// Aktionen
if ($mahnung->status !== Mahnung::STATUS_STORNIERT && $user->hasRight('mahnung', 'delete')) {
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
$form = new Form($db);
// --- Generierte Dokumente (wie bei Rechnungen) ---
print '<br>';
print load_fiche_titre($langs->trans('Documents'), '', 'fa-file');
// Dokumente im Rechnungsordner suchen die zur Mahnung gehoeren
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
$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);
}
$mahnungRef = dol_sanitizeFileName($mahnung->ref);
$fileList = array();
if (!empty($docDir) && is_dir($docDir)) {
$allFiles = dol_dir_list($docDir, 'files', 0, preg_quote($mahnungRef, '/'));
foreach ($allFiles as $f) {
$fileList[] = $f;
}
}
if (!empty($fileList)) {
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('Document').'</th>';
print '<th class="right">'.$langs->trans('Size').'</th>';
print '<th class="center">'.$langs->trans('Date').'</th>';
print '<th class="center"></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'; // Inline-Ansicht (keine Download-Erzwingung)
$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>';
// Groesse
print '<td class="right nowraponall">'.dol_print_size($filesize, 0, 0).'</td>';
// Datum
print '<td class="center nowraponall">'.dol_print_date($filedate, 'dayhour').'</td>';
// Aktionen: Vorschau + Download
print '<td class="right nowraponall">';
if ($ext === 'pdf') {
print '<a href="'.$viewUrl.'" target="_blank" title="'.dol_escape_htmltag($langs->trans('Preview')).'">'.img_picto($langs->trans('Preview'), 'search', 'class="pictofixedwidth"').'</a> ';
}
print '<a href="'.$dlUrl.'">'.img_picto($langs->trans('Download'), 'download', 'class="pictofixedwidth"').'</a>';
print '</td>';
print '</tr>';
}
print '</table>';
} else {
print '<div class="opacitymedium">'.$langs->trans('NoDocuments').'</div>';
}
// Aktionen
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
$form = new Form($db);
if ($mahnung->status !== Mahnung::STATUS_STORNIERT && $user->hasRight('mahnung', 'delete')) {
if ($action === 'confirm_storno') {
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id),
@ -126,12 +197,33 @@ if ($mahnung->status !== Mahnung::STATUS_STORNIERT && $user->hasRight('mahnung',
1
);
}
}
print '<br><div class="tabsAction">';
print '<br><div class="tabsAction">';
if ($user->hasRight('mahnung', 'write')) {
// 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> ';
}
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></div>';
print '</a>';
}
print '</div>';
llxFooter();
$db->close();

View file

@ -92,6 +92,9 @@ class Mahnung extends CommonObject
/** @var string */
public $note_private;
/** @var string Aktuelles Dokumentenmodell */
public $model_pdf;
/** @var int 0..9 */
public $status = self::STATUS_ENTWURF;
@ -250,10 +253,44 @@ class Mahnung extends CommonObject
$this->fk_user_creat = $obj->fk_user_creat;
$this->fk_user_modif = $obj->fk_user_modif;
// Alias fuer CommonObject::fetch_thirdparty()
$this->socid = $this->fk_soc;
$this->db->free($resql);
return 1;
}
/**
* Dokument generieren ueber das Dolibarr-Dokumentenmodell-System.
*
* @param string $modele Modellname ('standard_mahnung', 'generic_mahnung_odt')
* @param Translate $outputlangs Ausgabesprache
* @param int $hidedetails Details ausblenden
* @param int $hidedesc Beschreibung ausblenden
* @param int $hideref Referenz ausblenden
* @param array $moreparams Weitere Parameter
* @return int >0 OK, <=0 Fehler
*/
public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
{
global $conf, $langs;
$langs->load("mahnung@mahnung");
if (!dol_strlen($modele)) {
$modele = 'standard_mahnung';
if (!empty($this->model_pdf)) {
$modele = $this->model_pdf;
} elseif (getDolGlobalString('MAHNUNG_ADDON_PDF')) {
$modele = getDolGlobalString('MAHNUNG_ADDON_PDF');
}
}
$modelpath = "core/modules/mahnung/doc/";
return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
}
/**
* Mehrere Mahnungen laden.
*

View file

@ -0,0 +1,254 @@
<?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/core/boxes/box_mahnung_offen.php
* \ingroup mahnung
* \brief Widget: Aelteste offene Kundenrechnungen mit Mahnstufe.
*/
require_once DOL_DOCUMENT_ROOT.'/core/boxes/modules_boxes.php';
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
/**
* Widget: Offene Kundenrechnungen mit aktueller Mahnstufe.
*/
class box_mahnung_offen extends ModeleBoxes
{
public $boxcode = "mahnungoffenerechnungen";
public $boximg = "fa-envelope-open-text";
public $boxlabel = "MahnungBoxOffeneRechnungen";
public $depends = array("facture", "mahnung");
/**
* @param DoliDB $db
* @param string $param
*/
public function __construct($db, $param)
{
global $user;
$this->db = $db;
$this->hidden = !($user->hasRight('facture', 'lire'));
}
/**
* @param int $max
*/
public function loadBox($max = 5)
{
global $conf, $user, $langs;
$this->max = $max;
$langs->loadLangs(array('bills', 'mahnung@mahnung'));
$facturestatic = new Facture($this->db);
$societestatic = new Societe($this->db);
$this->info_box_head = array(
'text' => $langs->trans("MahnungBoxOffeneRechnungen", $this->max),
);
if (!$user->hasRight('facture', 'lire')) {
$this->info_box_contents[0][0] = array(
'td' => 'class="nohover left"',
'text' => '<span class="opacitymedium">'.$langs->trans("ReadPermissionNotAllowed").'</span>'
);
return;
}
// Offene Rechnungen mit optionalem LEFT JOIN auf letzte aktive Mahnung
$sql = "SELECT s.rowid as socid, s.nom as name, s.logo, s.email, s.entity,";
$sql .= " s.code_client, s.client,";
$sql .= " f.rowid as facid, f.ref, f.type, f.datef as date,";
$sql .= " f.date_lim_reglement as datelimit,";
$sql .= " f.total_ht, f.total_tva, f.total_ttc,";
$sql .= " f.paye, f.fk_statut as status,";
$sql .= " COALESCE(SUM(pf.amount), 0) as am,";
// Letzte Mahnstufe per Subquery
$sql .= " (SELECT m2.stufe FROM ".MAIN_DB_PREFIX."mahnung_mahnung as m2";
$sql .= " WHERE m2.fk_facture = f.rowid AND m2.status NOT IN (".Mahnung::STATUS_STORNIERT.")";
$sql .= " ORDER BY m2.stufe DESC LIMIT 1) as mahnstufe,";
$sql .= " (SELECT m3.date_mahnung FROM ".MAIN_DB_PREFIX."mahnung_mahnung as m3";
$sql .= " WHERE m3.fk_facture = f.rowid AND m3.status NOT IN (".Mahnung::STATUS_STORNIERT.")";
$sql .= " ORDER BY m3.stufe DESC LIMIT 1) as mahndatum";
$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = f.fk_soc";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."paiement_facture as pf ON f.rowid = pf.fk_facture";
$sql .= " WHERE f.entity IN (".getEntity('invoice').")";
$sql .= " AND f.paye = 0";
$sql .= " AND f.fk_statut = 1";
$sql .= " AND f.date_lim_reglement IS NOT NULL";
$sql .= " AND f.date_lim_reglement < '".$this->db->idate(dol_now())."'";
if (empty($user->socid) && !$user->hasRight('societe', 'client', 'voir')) {
$sql .= " AND s.rowid IN (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_user = ".((int) $user->id).")";
}
if ($user->socid) {
$sql .= " AND s.rowid = ".((int) $user->socid);
}
$sql .= " GROUP BY s.rowid, s.nom, s.logo, s.email, s.entity, s.code_client, s.client,";
$sql .= " f.rowid, f.ref, f.type, f.datef, f.date_lim_reglement,";
$sql .= " f.total_ht, f.total_tva, f.total_ttc, f.paye, f.fk_statut";
$sql .= " ORDER BY f.date_lim_reglement ASC, f.ref ASC";
$sql .= $this->db->plimit($this->max + 1, 0);
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php';
$result = $this->db->query($sql);
if (!$result) {
$this->info_box_contents[0][0] = array(
'td' => '', 'maxlength' => 500,
'text' => $this->db->error().' sql='.$sql,
);
return;
}
$num = $this->db->num_rows($result);
$line = 0;
$l_due_date = $langs->trans('Late').' ('.strtolower($langs->trans('DateDue')).': %s)';
$totalHt = 0;
while ($line < min($num, $this->max)) {
$objp = $this->db->fetch_object($result);
$datelimit = $this->db->jdate($objp->datelimit);
$tageVerzug = (int) floor((dol_now() - $datelimit) / 86400);
$facturestatic->id = $objp->facid;
$facturestatic->ref = $objp->ref;
$facturestatic->type = $objp->type;
$facturestatic->total_ht = $objp->total_ht;
$facturestatic->total_tva = $objp->total_tva;
$facturestatic->total_ttc = $objp->total_ttc;
$facturestatic->date = $this->db->jdate($objp->date);
$facturestatic->date_lim_reglement = $datelimit;
$facturestatic->statut = $objp->status;
$facturestatic->status = $objp->status;
$facturestatic->paye = $objp->paye;
$facturestatic->paid = $objp->paye;
$facturestatic->alreadypaid = $objp->am;
$facturestatic->totalpaid = $objp->am;
$societestatic->id = $objp->socid;
$societestatic->name = $objp->name;
$societestatic->code_client = $objp->code_client;
$societestatic->client = $objp->client;
$societestatic->logo = $objp->logo;
$societestatic->email = $objp->email;
$societestatic->entity = $objp->entity;
$late = '';
if ($facturestatic->hasDelay()) {
$late = img_warning(sprintf($l_due_date, dol_print_date($datelimit, 'day', 'tzuserrel')));
}
// Mahnstufe-Badge
$mahnBadge = '';
if (!empty($objp->mahnstufe)) {
$stufe = (int) $objp->mahnstufe;
$colors = array(1 => '#4a90d9', 2 => '#e68a00', 3 => '#cc3333');
$labels = array(1 => 'Stufe 1', 2 => 'Stufe 2', 3 => 'Stufe 3');
$color = $colors[$stufe] ?? '#666';
$label = $labels[$stufe] ?? 'Stufe '.$stufe;
$mahnDatum = $objp->mahndatum ? dol_print_date($this->db->jdate($objp->mahndatum), 'day') : '';
$tooltip = $label.($mahnDatum ? ' vom '.$mahnDatum : '');
$mahnBadge = ' <span class="badge" style="background-color:'.$color.';color:#fff;font-size:0.75em;" title="'.dol_escape_htmltag($tooltip).'">'.$label.'</span>';
}
// Spalte 1: Rechnung + Warnung
$this->info_box_contents[$line][] = array(
'td' => 'class="nowraponall"',
'text' => $facturestatic->getNomUrl(1),
'text2' => $late,
'asis' => 1,
);
// Spalte 2: Kunde
$this->info_box_contents[$line][] = array(
'td' => 'class="tdoverflowmax150 maxwidth150onsmartphone"',
'text' => $societestatic->getNomUrl(1, '', 44),
'asis' => 1,
);
// Spalte 3: Betrag
$this->info_box_contents[$line][] = array(
'td' => 'class="nowraponall right amount"',
'text' => price($objp->total_ht, 0, $langs, 0, -1, -1, $conf->currency),
);
// Spalte 4: Faelligkeitsdatum
$this->info_box_contents[$line][] = array(
'td' => 'class="center nowraponall" title="'.dol_escape_htmltag($langs->trans("DateDue").': '.dol_print_date($datelimit, 'day', 'tzuserrel')).'"',
'text' => dol_print_date($datelimit, 'day', 'tzuserrel'),
);
// Spalte 5: Tage Verzug
$this->info_box_contents[$line][] = array(
'td' => 'class="center nowraponall"',
'text' => $tageVerzug.' T.',
);
// Spalte 6: Mahnstufe
$this->info_box_contents[$line][] = array(
'td' => 'class="center nowraponall"',
'text' => !empty($mahnBadge) ? $mahnBadge : '<span class="opacitymedium">—</span>',
'asis' => 1,
);
$totalHt += (float) $objp->total_ht;
$line++;
}
if ($this->max < $num) {
$this->info_box_contents[$line][] = array('td' => 'colspan="6"', 'text' => '...');
$line++;
}
if ($num > 0) {
// Summenzeile
$this->info_box_contents[$line][] = array(
'tr' => 'class="liste_total"',
'td' => 'class="liste_total"',
'text' => $langs->trans("Total"),
);
$this->info_box_contents[$line][] = array(
'td' => 'class="liste_total"',
'text' => '&nbsp;',
);
$this->info_box_contents[$line][] = array(
'td' => 'class="nowraponall right liste_total"',
'text' => price($totalHt, 0, $langs, 0, -1, -1, $conf->currency),
);
$this->info_box_contents[$line][] = array(
'td' => 'class="liste_total"',
'text' => '&nbsp;',
);
$this->info_box_contents[$line][] = array(
'td' => 'class="liste_total"',
'text' => '&nbsp;',
);
$this->info_box_contents[$line][] = array(
'td' => 'class="liste_total"',
'text' => '&nbsp;',
);
}
$this->db->free($result);
}
/**
* @param array|null $head
* @param array|null $contents
* @param int $nooutput
* @return string
*/
public function showBox($head = null, $contents = null, $nooutput = 0)
{
return parent::showBox($this->info_box_head, $this->info_box_contents, $nooutput);
}
}

View file

@ -0,0 +1,504 @@
<?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/core/modules/mahnung/doc/doc_generic_mahnung_odt.modules.php
* \ingroup mahnung
* \brief ODT-Template-Generator fuer Mahnschreiben.
*
* Stufen-spezifische Templates: mahnung_stufe1.odt, mahnung_stufe2.odt, mahnung_stufe3.odt
* Fallback: beliebiges hochgeladenes .odt Template.
*/
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/mahnung/modules_mahnung.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/doc.lib.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/class/mahnung.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php';
/**
* ODT-Template-basierter Dokumentengenerator fuer Mahnungen.
*/
class doc_generic_mahnung_odt extends ModelePDFMahnung
{
/**
* @var string
*/
public $version = 'dolibarr';
/**
* @param DoliDB $db
*/
public function __construct($db)
{
global $langs, $mysoc;
$langs->loadLangs(array('main', 'companies'));
$this->db = $db;
$this->name = 'ODT templates';
$this->description = $langs->trans('DocumentModelOdt');
$this->scandir = 'MAHNUNG_ADDON_PDF_ODT_PATH';
$this->type = 'odt';
$this->page_largeur = 0;
$this->page_hauteur = 0;
$this->format = array($this->page_largeur, $this->page_hauteur);
$this->marge_gauche = 0;
$this->marge_droite = 0;
$this->marge_haute = 0;
$this->marge_basse = 0;
$this->option_logo = 1;
$this->option_multilang = 1;
$this->option_freetext = 1;
if ($mysoc !== null) {
$this->emetteur = $mysoc;
if (!$this->emetteur->country_code) {
$this->emetteur->country_code = substr($langs->defaultlang, -2);
}
}
}
/**
* Beschreibung und Template-Upload-Formular fuer die Admin-Seite.
*
* @param Translate $langs
* @return string
*/
public function info($langs)
{
global $conf;
$langs->loadLangs(array('errors', 'companies'));
$form = new Form($this->db);
$texte = $this->description.".<br>\n";
$texte .= '<form action="'.$_SERVER["PHP_SELF"].'" method="POST" enctype="multipart/form-data">';
$texte .= '<input type="hidden" name="token" value="'.newToken().'">';
$texte .= '<input type="hidden" name="page_y" value="">';
$texte .= '<input type="hidden" name="action" value="setModuleOptions">';
$texte .= '<input type="hidden" name="param1" value="MAHNUNG_ADDON_PDF_ODT_PATH">';
$texte .= '<table class="nobordernopadding" width="100%">';
// Verzeichnis-Liste
$texte .= '<tr><td valign="middle">';
$texttitle = $langs->trans("ListOfDirectories");
$listofdir = explode(',', preg_replace('/[\r\n]+/', ',', trim(getDolGlobalString('MAHNUNG_ADDON_PDF_ODT_PATH'))));
$listoffiles = array();
foreach ($listofdir as $key => $tmpdir) {
$tmpdir = trim($tmpdir);
$tmpdir = preg_replace('/DOL_DATA_ROOT/', DOL_DATA_ROOT, $tmpdir);
if (!$tmpdir) {
unset($listofdir[$key]);
continue;
}
if (!is_dir($tmpdir)) {
$texttitle .= img_warning($langs->trans("ErrorDirNotFound", $tmpdir), '');
} else {
$tmpfiles = dol_dir_list($tmpdir, 'files', 0, '\.(ods|odt)');
if (count($tmpfiles)) {
$listoffiles = array_merge($listoffiles, $tmpfiles);
}
}
}
$texthelp = $langs->trans("ListOfDirectoriesForModelGenODT");
$texthelp .= '<br><br><span class="opacitymedium">'.$langs->trans("ExampleOfDirectoriesForModelGen").'</span>';
$texthelp .= '<br>'.$langs->trans("FollowingSubstitutionKeysCanBeUsed").'<br>';
$texthelp .= '<br><b>Mahnung-spezifisch:</b><br>';
$texthelp .= '{mahnung_ref}, {mahnung_stufe}, {mahnung_stufe_label}, {mahnung_date},<br>';
$texthelp .= '{mahnung_betrag_offen}, {mahnung_mahngebuehr}, {mahnung_verzugszinsen},<br>';
$texthelp .= '{mahnung_summe}, {mahnung_basiszins}, {mahnung_zinssatz}, {mahnung_kundentyp},<br>';
$texthelp .= '{mahnung_pdf_intro}, {mahnung_date_lim_alt}, {mahnung_date_lim_neu}<br>';
$texthelp .= '<br><b>Rechnungsdaten:</b><br>';
$texthelp .= '{facture_ref}, {facture_date}, {facture_total_ttc}, {facture_already_paid}<br>';
$texthelp .= '<br><b>Bankdaten:</b><br>';
$texthelp .= '{mahnung_bank_label}, {mahnung_bank_iban}, {mahnung_bank_bic}<br>';
$texthelp .= '<br><b>Stufen-spezifische Templates:</b><br>';
$texthelp .= 'mahnung_stufe1.odt, mahnung_stufe2.odt, mahnung_stufe3.odt<br>';
$texte .= $form->textwithpicto($texttitle, $texthelp, 1, 'help', '', 1, 3, $this->name);
$texte .= '<div><div style="display: inline-block; min-width: 100px; vertical-align: middle;">';
$texte .= '<textarea class="flat textareafordir" spellcheck="false" cols="60" name="value1">';
$texte .= getDolGlobalString('MAHNUNG_ADDON_PDF_ODT_PATH');
$texte .= '</textarea>';
$texte .= '</div><div style="display: inline-block; vertical-align: middle;">';
$texte .= '<input type="submit" class="button button-edit reposition smallpaddingimp" name="modify" value="'.dol_escape_htmltag($langs->trans("Modify")).'">';
$texte .= '<br></div></div>';
// Gefundene Templates auflisten
$nbofiles = count($listoffiles);
if (getDolGlobalString('MAHNUNG_ADDON_PDF_ODT_PATH')) {
$texte .= $langs->trans("NumberOfModelFilesFound").': <b>'.count($listoffiles).'</b>';
}
if ($nbofiles) {
$texte .= '<div id="div_'.get_class($this).'">';
foreach ($listoffiles as $file) {
$texte .= '- '.$file['name'].' <a href="'.DOL_URL_ROOT.'/document.php?modulepart=doctemplates&file=mahnung/'.urlencode(basename($file['name'])).'">'.img_picto('', 'listlight').'</a>';
$texte .= ' &nbsp; <a class="reposition" href="'.$_SERVER["PHP_SELF"].'?modulepart=doctemplates&keyforuploaddir=MAHNUNG_ADDON_PDF_ODT_PATH&action=deletefile&token='.newToken().'&file='.urlencode(basename($file['name'])).'">'.img_picto('', 'delete').'</a>';
$texte .= '<br>';
}
$texte .= '</div>';
}
// Hinweis zur Benennung
$texte .= '<br><div class="opacitymedium" style="margin: 8px 0; padding: 6px 10px; border-left: 3px solid #4a90d9; background: rgba(74,144,217,0.05);">';
$texte .= '<b>Dateinamen-Konvention:</b><br>';
$texte .= '<code>mahnung_stufe1.odt</code> = Zahlungserinnerung (Stufe 1)<br>';
$texte .= '<code>mahnung_stufe2.odt</code> = 1. Mahnung (Stufe 2)<br>';
$texte .= '<code>mahnung_stufe3.odt</code> = Letzte Mahnung (Stufe 3)<br>';
$texte .= '<code>mahnung.odt</code> = Fallback fuer alle Stufen';
$texte .= '</div>';
// Upload-Feld
$texte .= '<div>'.$langs->trans("UploadNewTemplate");
$maxfilesizearray = getMaxFileSizeArray();
$maxmin = $maxfilesizearray['maxmin'];
if ($maxmin > 0) {
$texte .= '<input type="hidden" name="MAX_FILE_SIZE" value="'.($maxmin * 1024).'">';
}
$texte .= ' <input type="file" name="uploadfile">';
$texte .= '<input type="hidden" value="MAHNUNG_ADDON_PDF_ODT_PATH" name="keyforuploaddir">';
$texte .= '<input type="submit" class="button smallpaddingimp reposition" value="'.dol_escape_htmltag($langs->trans("Upload")).'" name="upload">';
$texte .= '</div>';
$texte .= '</td></tr>';
$texte .= '</table>';
$texte .= '</form>';
return $texte;
}
/**
* Erzeugt das ODT-Dokument aus einem Template.
*
* @param object $object Mahnung-Objekt
* @param Translate $outputlangs Ausgabesprache
* @param string $srctemplatepath Pfad zum ODT-Template
* @param int $hidedetails Details ausblenden
* @param int $hidedesc Beschreibung ausblenden
* @param int $hideref Referenz ausblenden
* @return int 1=OK, <=0=Fehler
*/
public function write_file($object, $outputlangs, $srctemplatepath = '', $hidedetails = 0, $hidedesc = 0, $hideref = 0)
{
global $user, $langs, $conf, $mysoc, $hookmanager;
if (empty($srctemplatepath)) {
dol_syslog("doc_generic_mahnung_odt::write_file srctemplatepath leer", LOG_WARNING);
return -1;
}
if (!is_object($hookmanager)) {
include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
$hookmanager = new HookManager($this->db);
}
$hookmanager->initHooks(array('odtgeneration'));
global $action;
if (!is_object($outputlangs)) {
$outputlangs = $langs;
}
$sav_charset_output = $outputlangs->charset_output;
$outputlangs->charset_output = 'UTF-8';
$outputlangs->loadLangs(array('main', 'dict', 'companies', 'bills', 'mahnung@mahnung'));
$mahnung = $object;
// Rechnung + Kunde + Stufe laden
$facture = new Facture($this->db);
if ($facture->fetch((int) $mahnung->fk_facture) <= 0) {
$this->error = 'Rechnung '.((int) $mahnung->fk_facture).' nicht ladbar.';
return -1;
}
$societe = new Societe($this->db);
if ($societe->fetch((int) $mahnung->fk_soc) <= 0) {
$this->error = 'Kunde '.((int) $mahnung->fk_soc).' nicht ladbar.';
return -1;
}
// Fuer die Standard-Substitution-Arrays
$mahnung->thirdparty = $societe;
$stufeObj = new MahnungStufe($this->db);
$stufeObj->fetchByStufe((int) $mahnung->stufe);
// Stufen-spezifisches Template suchen
$srctemplatepath = $this->findStufeTemplate($srctemplatepath, (int) $mahnung->stufe);
// Ausgabe-Verzeichnis: im Facture-Ordner (damit es im Rechnung-Tab sichtbar ist)
$dirFact = !empty($conf->facture->multidir_output[$facture->entity])
? $conf->facture->multidir_output[$facture->entity]
: $conf->facture->dir_output;
$dir = $dirFact.'/'.dol_sanitizeFileName($facture->ref);
if (!file_exists($dir)) {
if (dol_mkdir($dir) < 0) {
$this->error = $outputlangs->transnoentities("ErrorCanNotCreateDir", $dir);
return -1;
}
}
// Dateiname
$newfile = basename($srctemplatepath);
$newfiletmp = preg_replace('/\.od[ts]/i', '', $newfile);
$newfiletmp = preg_replace('/template_/i', '', $newfiletmp);
$newfiletmp = preg_replace('/modele_/i', '', $newfiletmp);
$objectref = dol_sanitizeFileName($mahnung->ref);
$newfiletmp = $objectref.'_'.$newfiletmp;
$newfileformat = substr($newfile, strrpos($newfile, '.') + 1);
if (getDolGlobalString('MAIN_DOC_USE_TIMING')) {
$format = getDolGlobalString('MAIN_DOC_USE_TIMING');
if ($format == '1') {
$format = '%Y%m%d%H%M%S';
}
$filename = $newfiletmp.'-'.dol_print_date(dol_now(), $format).'.'.$newfileformat;
} else {
$filename = $newfiletmp.'.'.$newfileformat;
}
$file = $dir.'/'.$filename;
// Temp-Verzeichnis
$tmpdir = $conf->mahnung->dir_temp ?? ($conf->facture->dir_temp ?? DOL_DATA_ROOT.'/mahnung/temp');
dol_mkdir($tmpdir);
// ODT oeffnen
require_once ODTPHP_PATH.'odf.php';
try {
$odfHandler = new Odf(
$srctemplatepath,
array(
'PATH_TO_TMP' => $tmpdir,
'ZIP_PROXY' => getDolGlobalString('MAIN_ODF_ZIP_PROXY', 'PclZipProxy'),
'DELIMITER_LEFT' => '{',
'DELIMITER_RIGHT' => '}'
)
);
} catch (Exception $e) {
$this->error = $e->getMessage();
dol_syslog($e->getMessage(), LOG_ERR);
return -1;
}
// Substitution-Arrays zusammenbauen
$substitutionarray = array(
'__FROM_NAME__' => $this->emetteur->name,
'__FROM_EMAIL__' => $this->emetteur->email,
);
complete_substitutions_array($substitutionarray, $outputlangs, $mahnung);
// Standard-Arrays
$array_common = getCommonSubstitutionArray($outputlangs, 0, null, $mahnung);
$array_object_from_properties = $this->get_substitutionarray_each_var_object($mahnung, $outputlangs);
$array_objet = $this->get_substitutionarray_object($mahnung, $outputlangs);
$array_user = $this->get_substitutionarray_user($user, $outputlangs);
$array_soc = $this->get_substitutionarray_mysoc($mysoc, $outputlangs);
$array_thirdparty = $this->get_substitutionarray_thirdparty($societe, $outputlangs);
$array_other = $this->get_substitutionarray_other($outputlangs);
// Mahnung-spezifische Tags
$array_mahnung = $this->getMahnungSubstitutionArray($mahnung, $facture, $stufeObj, $outputlangs);
// Alles zusammenfuehren
$tmparray = array_merge($substitutionarray, $array_common, $array_object_from_properties, $array_user, $array_soc, $array_thirdparty, $array_objet, $array_other, $array_mahnung);
complete_substitutions_array($tmparray, $outputlangs, $mahnung);
// Hook
$parameters = array('odfHandler' => &$odfHandler, 'file' => $file, 'object' => $mahnung, 'outputlangs' => $outputlangs, 'substitutionarray' => &$tmparray);
$hookmanager->executeHooks('ODTSubstitution', $parameters, $this, $action);
// Variablen ins ODT schreiben
foreach ($tmparray as $key => $value) {
try {
if (preg_match('/logo$/', $key)) {
if (file_exists($value)) {
$odfHandler->setImage($key, $value);
} else {
$odfHandler->setVars($key, 'ErrorFileNotFound', true, 'UTF-8');
}
} else {
$odfHandler->setVars($key, $value, true, 'UTF-8');
}
} catch (OdfException $e) {
dol_syslog($e->getMessage(), LOG_INFO);
}
}
// Uebersetzungs-Labels
$tmparray2 = $outputlangs->get_translations_for_substitutions();
foreach ($tmparray2 as $key => $value) {
try {
$odfHandler->setVars($key, $value, true, 'UTF-8');
} catch (OdfException $e) {
dol_syslog($e->getMessage(), LOG_INFO);
}
}
// Speichern (als PDF wenn MAIN_ODT_AS_PDF gesetzt)
if (getDolGlobalString('MAIN_ODT_AS_PDF')) {
try {
$odfHandler->exportAsAttachedPDF($file);
} catch (Exception $e) {
$this->error = $e->getMessage();
dol_syslog($e->getMessage(), LOG_ERR);
return -1;
}
} else {
try {
$odfHandler->saveToDisk($file);
} catch (Exception $e) {
$this->error = $e->getMessage();
dol_syslog($e->getMessage(), LOG_ERR);
return -1;
}
}
dolChmod($file);
$odfHandler = null;
// Pfad in DB persistieren
$mahnung->pdf_path = $file;
$mahnung->update($user);
$this->result = array('fullpath' => $file);
$outputlangs->charset_output = $sav_charset_output;
return 1;
}
/**
* Sucht ein stufen-spezifisches Template (mahnung_stufe{N}.odt).
* Fallback: das uebergebene generische Template.
*
* @param string $srctemplatepath Generisches Template (von commonGenerateDocument)
* @param int $stufe Mahnstufe 1-3
* @return string Pfad zum besten Template
*/
private function findStufeTemplate($srctemplatepath, $stufe)
{
global $conf;
$listofdir = explode(',', preg_replace('/[\r\n]+/', ',', trim(getDolGlobalString('MAHNUNG_ADDON_PDF_ODT_PATH'))));
foreach ($listofdir as $tmpdir) {
$tmpdir = trim(preg_replace('/DOL_DATA_ROOT/', DOL_DATA_ROOT, $tmpdir));
if (empty($tmpdir)) {
continue;
}
$stufeFile = $tmpdir.'/mahnung_stufe'.$stufe.'.odt';
if (file_exists($stufeFile)) {
return $stufeFile;
}
}
return $srctemplatepath;
}
/**
* Mahnung-spezifische Substitution-Variablen.
*
* @param Mahnung $mahnung
* @param Facture $facture
* @param MahnungStufe $stufe
* @param Translate $outputlangs
* @return array
*/
private function getMahnungSubstitutionArray($mahnung, $facture, $stufe, $outputlangs)
{
$gezahlt = (float) $facture->total_ttc - (float) $mahnung->betrag_offen;
$basiszins = $mahnung->basiszins_snapshot !== null ? (float) $mahnung->basiszins_snapshot : 0.0;
$aufschlag = $mahnung->customertype === Mahnung::KUNDENTYP_B2B
? (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2B', '9.0')
: (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2C', '5.0');
$intro = (string) ($stufe->pdf_intro ?? '');
if (empty($intro)) {
$intro = $this->defaultIntro((int) $mahnung->stufe);
}
$bank = $this->getDefaultBankData();
return array(
'mahnung_ref' => $mahnung->ref,
'mahnung_stufe' => (int) $mahnung->stufe,
'mahnung_stufe_label' => $stufe->label ?? '',
'mahnung_date' => dol_print_date($mahnung->date_mahnung, 'day'),
'mahnung_date_lim_alt' => dol_print_date($mahnung->date_lim_reglement_alt, 'day'),
'mahnung_date_lim_neu' => dol_print_date($mahnung->date_lim_reglement_neu, 'day'),
'mahnung_betrag_offen' => price((float) $mahnung->betrag_offen),
'mahnung_mahngebuehr' => price((float) $mahnung->mahngebuehr),
'mahnung_pauschale_b2b' => price((float) $mahnung->pauschale_b2b),
'mahnung_verzugszinsen' => price((float) $mahnung->verzugszinsen),
'mahnung_summe' => price((float) $mahnung->summe_mahnung),
'mahnung_basiszins' => number_format($basiszins, 2, ',', '.'),
'mahnung_zinssatz' => number_format($basiszins + $aufschlag, 2, ',', '.'),
'mahnung_kundentyp' => $mahnung->customertype,
'mahnung_versandart' => $mahnung->versandart ?? '',
'mahnung_pdf_intro' => $intro,
// Rechnungsdaten
'facture_ref' => $facture->ref,
'facture_date' => dol_print_date($facture->date, 'day'),
'facture_date_lim' => dol_print_date($facture->date_lim_reglement, 'day'),
'facture_total_ht' => price((float) $facture->total_ht),
'facture_total_ttc' => price((float) $facture->total_ttc),
'facture_total_tva' => price((float) $facture->total_tva),
'facture_already_paid' => price($gezahlt),
// Bank
'mahnung_bank_label' => $bank['label'] ?? '',
'mahnung_bank_iban' => $bank['iban'] ?? '',
'mahnung_bank_bic' => $bank['bic'] ?? '',
);
}
/**
* Standard-Bankkonto-Daten.
*
* @return array
*/
private function getDefaultBankData()
{
$sql = "SELECT label, iban_prefix as iban, bic FROM ".MAIN_DB_PREFIX."bank_account";
$sql .= " WHERE clos = 0 AND default_rib = 1 LIMIT 1";
$resql = $this->db->query($sql);
if (!$resql || !$this->db->num_rows($resql)) {
return array();
}
$obj = $this->db->fetch_object($resql);
$this->db->free($resql);
return array('label' => $obj->label, 'iban' => $obj->iban, 'bic' => $obj->bic);
}
/**
* Default-Intro je Stufe.
*
* @param int $stufe
* @return string
*/
private function defaultIntro($stufe)
{
switch ((int) $stufe) {
case 1:
return 'unsere unten aufgefuehrte Rechnung ist trotz Ablauf der Zahlungsfrist noch nicht beglichen. '
. 'Vielleicht ist Ihnen dies entgangen — wir bitten Sie hoeflich, den ausstehenden Betrag zeitnah zu ueberweisen.';
case 2:
return 'leider mussten wir feststellen, dass die unten aufgefuehrte Rechnung trotz unserer '
. 'Zahlungserinnerung weiterhin offen ist. Wir bitten Sie nun nachdruecklich um Begleichung '
. 'des offenen Betrags zuzueglich Verzugszinsen und Mahnkosten.';
case 3:
default:
return 'wir haben Sie bereits zweimal an die Begleichung der unten aufgefuehrten Rechnung erinnert. '
. 'Sollte der offene Betrag inkl. Verzugszinsen und Mahnkosten nicht innerhalb der angegebenen Frist '
. 'auf unserem Konto eingehen, sehen wir uns gezwungen, weitere rechtliche Schritte einzuleiten.';
}
}
}

View file

@ -6,96 +6,107 @@
*/
/**
* \file htdocs/custom/mahnung/class/mahnungpdf.class.php
* \file htdocs/custom/mahnung/core/modules/mahnung/doc/pdf_standard_mahnung.modules.php
* \ingroup mahnung
* \brief PDF-Generator fuer Mahnschreiben (DIN-5008 Form A).
*
* Nutzt Dolibarrs TCPDF-Wrapper (pdf_getInstance) und schreibt das fertige
* PDF in den Dokumenten-Ordner der Original-Rechnung
* documents/facture/{ref-rechnung}/mahnung-{stufe}-{ref-mahnung}.pdf
* Damit erscheint die Mahnung automatisch im Dokumente-Tab der Rechnung.
* \brief Standard-PDF-Generator (TCPDF, DIN-5008) fuer Mahnschreiben.
*/
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/mahnung/modules_mahnung.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.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/class/mahnung.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php';
class MahnungPdf
/**
* Standard-PDF-Generator fuer Mahnungen (DIN-5008 Form A, TCPDF).
*/
class pdf_standard_mahnung extends ModelePDFMahnung
{
/** @var DoliDB */
public $db;
/** @var string */
public $error = '';
/**
* @param DoliDB $db
*/
public function __construct($db)
{
global $langs;
$this->db = $db;
$this->name = 'standard_mahnung';
$this->description = $langs->trans('MahnungPdfStandard');
$this->type = 'pdf';
$this->page_largeur = 210;
$this->page_hauteur = 297;
$this->format = array($this->page_largeur, $this->page_hauteur);
$this->marge_gauche = 25;
$this->marge_droite = 25;
$this->marge_haute = 25;
$this->marge_basse = 25;
}
/**
* Erzeugt das PDF zum Mahnvorgang. Setzt $mahnung->pdf_path nach Erfolg
* und schreibt sie via update($user) in die DB.
*
* @param Mahnung $mahnung
* @param User $user
* @return string|false Absoluter Pfad zur PDF-Datei oder false bei Fehler
* @param object $object Mahnung-Objekt
* @param Translate $outputlangs Ausgabesprache
* @param string $srctemplatepath (nicht verwendet bei TCPDF)
* @param int $hidedetails Details ausblenden
* @param int $hidedesc Beschreibung ausblenden
* @param int $hideref Referenz ausblenden
* @return int 1=OK, <=0=Fehler
*/
public function generate(Mahnung $mahnung, $user)
public function write_file($object, $outputlangs, $srctemplatepath = '', $hidedetails = 0, $hidedesc = 0, $hideref = 0)
{
global $conf, $langs, $mysoc;
global $conf, $langs, $mysoc, $user;
$langs->loadLangs(array('main', 'bills', 'companies', 'mahnung@mahnung'));
if (!is_object($outputlangs)) {
$outputlangs = $langs;
}
$outputlangs->loadLangs(array('main', 'bills', 'companies', 'mahnung@mahnung'));
// Original-Rechnung + Kunde laden
$mahnung = $object;
// Rechnung + Kunde laden
$facture = new Facture($this->db);
if ($facture->fetch((int) $mahnung->fk_facture) <= 0) {
$this->error = 'Rechnung '.((int) $mahnung->fk_facture).' nicht ladbar.';
return false;
return -1;
}
$societe = new Societe($this->db);
if ($societe->fetch((int) $mahnung->fk_soc) <= 0) {
$this->error = 'Kunde '.((int) $mahnung->fk_soc).' nicht ladbar.';
return false;
return -1;
}
$stufeObj = new MahnungStufe($this->db);
if ($stufeObj->fetchByStufe((int) $mahnung->stufe) <= 0) {
$this->error = 'Mahnstufe '.((int) $mahnung->stufe).' nicht konfiguriert.';
return false;
return -1;
}
// Ziel-Verzeichnis im Doc-Ordner der Rechnung
$dirOutput = $this->getOutputDir($facture);
if (!dol_mkdir($dirOutput)) {
// Ziel-Verzeichnis
$dirOutput = $this->getOutputDir($mahnung, $facture);
if (dol_mkdir($dirOutput) < 0) {
$this->error = 'Kann Verzeichnis nicht anlegen: '.$dirOutput;
return false;
return -1;
}
$filename = 'mahnung-'.((int) $mahnung->stufe).'-'.dol_sanitizeFileName($mahnung->ref).'.pdf';
$absPath = $dirOutput.'/'.$filename;
// PDF-Instanz
$pdf = pdf_getInstance(array('210', '297'));
$default_font_size = pdf_getPDFFontSize($langs);
$default_font_size = pdf_getPDFFontSize($outputlangs);
$pdf->SetAutoPageBreak(true, 25);
$pdf->SetFont(pdf_getPDFFont($langs), '', $default_font_size);
$pdf->SetFont(pdf_getPDFFont($outputlangs), '', $default_font_size);
$pdf->SetTitle($langs->trans('MahnungStufe').' '.((int) $mahnung->stufe).' — '.$facture->ref);
$pdf->SetSubject($langs->trans('MahnungRef').' '.$mahnung->ref);
$pdf->SetTitle($outputlangs->trans('MahnungStufe').' '.((int) $mahnung->stufe).' — '.$facture->ref);
$pdf->SetSubject($outputlangs->trans('MahnungRef').' '.$mahnung->ref);
$pdf->SetAuthor((string) $mysoc->name);
$pdf->SetCreator('Dolibarr Mahnung-Modul');
$pdf->Open();
$pdf->AddPage();
$this->renderHeader($pdf, $facture, $societe, $mahnung, $stufeObj);
$this->renderBody($pdf, $facture, $societe, $mahnung, $stufeObj);
$this->renderHeader($pdf, $facture, $societe, $mahnung, $stufeObj, $outputlangs);
$this->renderBody($pdf, $facture, $societe, $mahnung, $stufeObj, $outputlangs);
$this->renderFooter($pdf);
$pdf->Output($absPath, 'F');
@ -104,30 +115,22 @@ class MahnungPdf
$mahnung->pdf_path = $absPath;
$mahnung->update($user);
return $absPath;
$this->result = array('fullpath' => $absPath);
return 1;
}
/**
* Adressfenster, Datum, Betreff (DIN-5008 Form A: Adressfeld 45mm hoch ab 27mm).
*
* @param TCPDF $pdf
* @param Facture $facture
* @param Societe $societe
* @param Mahnung $mahnung
* @param MahnungStufe $stufe
* @return void
* Adressfenster, Datum, Betreff (DIN-5008 Form A).
*/
private function renderHeader($pdf, $facture, $societe, $mahnung, $stufe)
private function renderHeader($pdf, $facture, $societe, $mahnung, $stufe, $outputlangs)
{
global $langs, $mysoc;
global $mysoc;
// Absender klein im Adressfeld (Faltmarke darueber, oben in DIN 5008 Adresszeile)
$pdf->SetFont('helvetica', '', 7);
$pdf->SetXY(25, 50);
$senderLine = trim(($mysoc->name ?? '').' · '.($mysoc->address ?? '').' · '.($mysoc->zip ?? '').' '.($mysoc->town ?? ''));
$pdf->Cell(85, 4, $senderLine, 0, 1, 'L');
// Empfaenger-Block (Adressfenster: links 25mm, ab y=55)
$pdf->SetFont('helvetica', '', 11);
$pdf->SetXY(25, 55);
$lines = array();
@ -149,47 +152,34 @@ class MahnungPdf
$pdf->SetX(25);
}
// Bezugszeichen-Zeile rechts (DIN-5008): Datum + Mahn-Nr.
$pdf->SetFont('helvetica', '', 9);
$pdf->SetXY(125, 50);
$pdf->Cell(60, 4, $langs->trans('Date').': '.dol_print_date($mahnung->date_mahnung, 'day'), 0, 1, 'L');
$pdf->Cell(60, 4, $outputlangs->trans('Date').': '.dol_print_date($mahnung->date_mahnung, 'day'), 0, 1, 'L');
$pdf->SetX(125);
$pdf->Cell(60, 4, $langs->trans('MahnungRef').': '.$mahnung->ref, 0, 1, 'L');
$pdf->Cell(60, 4, $outputlangs->trans('MahnungRef').': '.$mahnung->ref, 0, 1, 'L');
$pdf->SetX(125);
$pdf->Cell(60, 4, $langs->trans('MahnungRechnung').': '.$facture->ref, 0, 1, 'L');
$pdf->Cell(60, 4, $outputlangs->trans('MahnungRechnung').': '.$facture->ref, 0, 1, 'L');
// Betreff
$pdf->SetXY(25, 100);
$pdf->SetFont('helvetica', 'B', 12);
$betreff = $stufe->label.' — '.$langs->trans('MahnungRechnung').' '.$facture->ref;
$betreff = $stufe->label.' — '.$outputlangs->trans('MahnungRechnung').' '.$facture->ref;
$pdf->Cell(0, 6, $betreff, 0, 1, 'L');
}
/**
* Anrede, Intro, Tabelle (Rechnung/Datum/Betrag/gezahlt/offen),
* Gebuehrenblock, Gesamtsumme, neue Frist, Bankverbindung.
*
* @param TCPDF $pdf
* @param Facture $facture
* @param Societe $societe
* @param Mahnung $mahnung
* @param MahnungStufe $stufe
* @return void
* Anrede, Intro, Tabelle, Gebuehrenblock, Gesamtsumme, neue Frist.
*/
private function renderBody($pdf, $facture, $societe, $mahnung, $stufe)
private function renderBody($pdf, $facture, $societe, $mahnung, $stufe, $outputlangs)
{
global $langs, $mysoc;
global $mysoc;
$pdf->SetFont('helvetica', '', 11);
$pdf->SetXY(25, 110);
// Anrede
$anrede = 'Sehr geehrte Damen und Herren,';
$pdf->Cell(0, 5, $anrede, 0, 1, 'L');
$pdf->Cell(0, 5, 'Sehr geehrte Damen und Herren,', 0, 1, 'L');
$pdf->SetX(25);
$pdf->Ln(2);
// Intro aus Stufen-Konfig (Fallback Default-Text je Stufe)
$intro = (string) $stufe->pdf_intro;
if (empty($intro)) {
$intro = $this->defaultIntro((int) $mahnung->stufe);
@ -201,11 +191,11 @@ class MahnungPdf
// Rechnungs-Tabelle
$pdf->SetFont('helvetica', 'B', 10);
$pdf->SetX(25);
$pdf->Cell(40, 6, $langs->trans('MahnungRechnung'), 'B', 0, 'L');
$pdf->Cell(30, 6, $langs->trans('Date'), 'B', 0, 'L');
$pdf->Cell(30, 6, $langs->trans('TotalTTC'), 'B', 0, 'R');
$pdf->Cell(30, 6, $langs->trans('AlreadyPaid'), 'B', 0, 'R');
$pdf->Cell(30, 6, $langs->trans('MahnungBetragOffen'), 'B', 1, 'R');
$pdf->Cell(40, 6, $outputlangs->trans('MahnungRechnung'), 'B', 0, 'L');
$pdf->Cell(30, 6, $outputlangs->trans('Date'), 'B', 0, 'L');
$pdf->Cell(30, 6, $outputlangs->trans('TotalTTC'), 'B', 0, 'R');
$pdf->Cell(30, 6, $outputlangs->trans('AlreadyPaid'), 'B', 0, 'R');
$pdf->Cell(30, 6, $outputlangs->trans('MahnungBetragOffen'), 'B', 1, 'R');
$pdf->SetFont('helvetica', '', 10);
$pdf->SetX(25);
@ -220,16 +210,16 @@ class MahnungPdf
// Gebuehrenblock
$pdf->SetX(25);
$pdf->SetFont('helvetica', '', 10);
$pdf->Cell(130, 6, $langs->trans('MahnungBetragOffen'), 0, 0, 'L');
$pdf->Cell(130, 6, $outputlangs->trans('MahnungBetragOffen'), 0, 0, 'L');
$pdf->Cell(30, 6, price((float) $mahnung->betrag_offen).' EUR', 0, 1, 'R');
if ((float) $mahnung->mahngebuehr > 0) {
$pdf->SetX(25);
$pdf->Cell(130, 6, $langs->trans('MahnungGebuehr'), 0, 0, 'L');
$pdf->Cell(130, 6, $outputlangs->trans('MahnungGebuehr'), 0, 0, 'L');
$pdf->Cell(30, 6, price((float) $mahnung->mahngebuehr).' EUR', 0, 1, 'R');
}
if ((float) $mahnung->pauschale_b2b > 0) {
$pdf->SetX(25);
$pdf->Cell(130, 6, $langs->trans('MahnungPauschaleB2B').' (BGB §288 Abs. 5)', 0, 0, 'L');
$pdf->Cell(130, 6, $outputlangs->trans('MahnungPauschaleB2B').' (BGB §288 Abs. 5)', 0, 0, 'L');
$pdf->Cell(30, 6, price((float) $mahnung->pauschale_b2b).' EUR', 0, 1, 'R');
}
if ((float) $mahnung->verzugszinsen > 0) {
@ -239,7 +229,7 @@ class MahnungPdf
? (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2B', '9.0')
: (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2C', '5.0');
$satz = $basis + $auf;
$pdf->Cell(130, 6, $langs->trans('MahnungVerzugszinsen').' ('.number_format($satz, 2, ',', '.').' %)', 0, 0, 'L');
$pdf->Cell(130, 6, $outputlangs->trans('MahnungVerzugszinsen').' ('.number_format($satz, 2, ',', '.').' %)', 0, 0, 'L');
$pdf->Cell(30, 6, price((float) $mahnung->verzugszinsen).' EUR', 0, 1, 'R');
}
@ -247,7 +237,7 @@ class MahnungPdf
$pdf->Ln(1);
$pdf->SetX(25);
$pdf->SetFont('helvetica', 'B', 11);
$pdf->Cell(130, 7, $langs->trans('MahnungSumme'), 'T', 0, 'L');
$pdf->Cell(130, 7, $outputlangs->trans('MahnungSumme'), 'T', 0, 'L');
$pdf->Cell(30, 7, price((float) $mahnung->summe_mahnung).' EUR', 'T', 1, 'R');
$pdf->Ln(5);
@ -270,9 +260,6 @@ class MahnungPdf
/**
* Fusszeile mit Bankverbindung + Firmen-Footer.
*
* @param TCPDF $pdf
* @return void
*/
private function renderFooter($pdf)
{
@ -288,10 +275,9 @@ class MahnungPdf
if (!empty($mysoc->phone)) {
$lines[] = 'Tel: '.$mysoc->phone;
}
// Bankverbindung aus Standard-Bankaccount
$bankAccount = $this->getDefaultBankLine();
if (!empty($bankAccount)) {
$lines[] = $bankAccount;
$bankLine = $this->getDefaultBankLine();
if (!empty($bankLine)) {
$lines[] = $bankLine;
}
foreach ($lines as $l) {
$pdf->Cell(0, 4, $l, 0, 1, 'C');
@ -299,7 +285,7 @@ class MahnungPdf
}
/**
* Standard-Bankkonto in einer Zeile (Bank · IBAN · BIC).
* Standard-Bankkonto in einer Zeile.
*
* @return string
*/
@ -319,7 +305,7 @@ class MahnungPdf
}
/**
* Default-Intro je Stufe (wenn Setup leer ist).
* Default-Intro je Stufe.
*
* @param int $stufe
* @return string
@ -344,12 +330,13 @@ class MahnungPdf
}
/**
* Ziel-Verzeichnis: documents/facture/{ref}/
* Ziel-Verzeichnis: documents/facture/{ref}/ (damit die Mahnung im Rechnung-Tab erscheint).
*
* @param Mahnung $mahnung
* @param Facture $facture
* @return string
*/
private function getOutputDir($facture)
private function getOutputDir($mahnung, $facture)
{
global $conf;
$documentDir = !empty($conf->facture->multidir_output[$facture->entity])

View file

@ -0,0 +1,55 @@
<?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/core/modules/mahnung/modules_mahnung.php
* \ingroup mahnung
* \brief Abstrakte Basis-Klasse fuer Mahnung-Dokumentengeneratoren.
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commondocgenerator.class.php';
/**
* Elternklasse der Mahnung-Dokumentengeneratoren (PDF, ODT).
*/
abstract class ModelePDFMahnung extends CommonDocGenerator
{
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Gibt die Liste der aktiven Dokumentenmodelle zurueck.
*
* @param DoliDB $db Datenbank-Handler
* @param int $maxfilenamelength Max Laenge
* @return array|int
*/
public static function liste_modeles($db, $maxfilenamelength = 0)
{
// phpcs:enable
$type = 'mahnung';
$list = array();
include_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
$list = getListOfModels($db, $type, $maxfilenamelength);
return $list;
}
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Erzeugt das Dokument auf der Festplatte.
*
* @param object $object Mahnung-Objekt
* @param Translate $outputlangs Ausgabesprache
* @param string $srctemplatepath Pfad zum Template (ODT)
* @param int $hidedetails Details ausblenden
* @param int $hidedesc Beschreibung ausblenden
* @param int $hideref Referenz ausblenden
* @return int 1=OK, <=0=Fehler
*/
abstract public function write_file($object, $outputlangs, $srctemplatepath = '', $hidedetails = 0, $hidedesc = 0, $hideref = 0);
// phpcs:enable
}

View file

@ -69,7 +69,7 @@ class modMahnung extends DolibarrModules
'menus' => 0,
'tpl' => 0,
'barcode' => 0,
'models' => 0,
'models' => 1,
'printing' => 0,
'theme' => 0,
'css' => array(),
@ -154,6 +154,24 @@ class modMahnung extends DolibarrModules
'allentities',
1,
),
5 => array(
'MAHNUNG_ADDON_PDF',
'chaine',
'standard_mahnung',
'Standard-Dokumentenmodell fuer Mahnungen',
0,
'current',
1,
),
6 => array(
'MAHNUNG_ADDON_PDF_ODT_PATH',
'chaine',
'DOL_DATA_ROOT/doctemplates/mahnung',
'Verzeichnis fuer ODT-Templates',
0,
'current',
1,
),
);
if (!isModEnabled('mahnung')) {
@ -166,7 +184,12 @@ class modMahnung extends DolibarrModules
$this->dictionaries = array();
$this->boxes = array();
$this->boxes = array(
0 => array(
'file' => 'box_mahnung_offen@mahnung',
'enabledbydefaulton' => 'Home',
),
);
// Cron-Job: Vorschlagsliste taeglich 06:00
$this->cronjobs = array(
@ -281,13 +304,25 @@ class modMahnung extends DolibarrModules
*/
public function init($options = '')
{
global $conf;
// Tabellen anlegen aus sql/-Verzeichnis
$result = $this->_load_tables('/mahnung/sql/');
if ($result < 0) {
return -1;
}
// Dokumentenmodelle registrieren
$sql = array();
$sql[] = "DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = 'standard_mahnung' AND type = 'mahnung' AND entity = ".((int) $conf->entity);
$sql[] = "INSERT INTO ".MAIN_DB_PREFIX."document_model (nom, type, entity, libelle, description) VALUES ('standard_mahnung', 'mahnung', ".((int) $conf->entity).", 'Standard PDF (DIN 5008)', NULL)";
$sql[] = "DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = 'generic_mahnung_odt' AND type = 'mahnung' AND entity = ".((int) $conf->entity);
$sql[] = "INSERT INTO ".MAIN_DB_PREFIX."document_model (nom, type, entity, libelle, description) VALUES ('generic_mahnung_odt', 'mahnung', ".((int) $conf->entity).", 'ODT templates', 'MAHNUNG_ADDON_PDF_ODT_PATH')";
// ODT-Template-Verzeichnis anlegen
$doctemplatedir = DOL_DATA_ROOT.'/doctemplates/mahnung';
dol_mkdir($doctemplatedir);
return $this->_init($sql, $options);
}

View file

@ -109,3 +109,16 @@ MahnungSettingsSaved = Einstellungen gespeichert.
#
MahnungCronBuildVorschlag = Mahnwesen — Vorschlagsliste aufbauen
MahnungCronBuildVorschlagDesc = Sucht taeglich ueberfaellige Rechnungen und sendet einen Ntfy-Push mit der Anzahl neuer Vorschlaege.
#
# Widget
#
MahnungBoxOffeneRechnungen = Ueberfaellige Rechnungen mit Mahnstufe (%s)
#
# Dokumentenmodelle
#
MahnungDokumentModelle = Dokumentenmodelle
MahnungPdfStandard = Standard-PDF (DIN 5008)
MahnungGenerate = Dokument generieren
NoDocuments = Keine Dokumente vorhanden.