* * GPL v3 (siehe COPYING). */ /** * \file htdocs/custom/mahnung/ajax/sammelbrief.php * \ingroup mahnung * \brief AJAX/Form-Endpoint: Sammelbrief — für eine Auswahl von Rechnungen * Mahnungen erzeugen und alle Einzel-PDFs in EIN PDF zusammenfassen. * * Kostenlose Zahlungserinnerungen (ist_erinnerung = 1) sind hier * ausgeschlossen: sie erzeugen kein Mahn-PDF und werden ausschließlich * per E-Mail mit der Original-Rechnung als Anhang versendet. * * POST: * facture_ids[] Rechnungs-IDs * stufe (opt) Stufe erzwingen (sonst Vorschlag) * token CSRF * * Response: PDF-Download "sammelbrief-YYYYMMDD.pdf". */ if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1'); if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1'); ob_start(); require_once $_SERVER['DOCUMENT_ROOT'].'/main.inc.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.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/class/mahnungvorschlag.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/lib/mahnung_anlage.lib.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/modMahnung.class.php'; global $db, $user, $langs, $conf; $langs->loadLangs(array('mahnung@mahnung')); // Schema nach einem reinen Datei-Deploy nachziehen (der Sammelbrief legt // Mahnvorgänge an und braucht dafür die Spalte kosten_vorstufen). modMahnung::ensureSchema($db); // CSRF $postedToken = GETPOST('token', 'alphanohtml'); if (empty($postedToken) || empty($_SESSION['newtoken']) || $postedToken !== $_SESSION['newtoken']) { httpExitError(403, $langs->trans('MahnungSammelbriefCsrfFehler')); } // Permission: der Sammelbrief LEGT Mahnvorgänge an (mahnungBaueVorgang() + // Mahnung::create()) und erzeugt die zugehörigen PDFs — das ist eine schreibende // Aktion. 'send' allein reicht dafür nicht: dieses Recht deckt nur das Versenden // bereits vorhandener Mahnungen ab. Die Liste blendet den Sammelbrief-Button // ohnehin nur bei mahnung.write ein (list.php: $canWrite), der Endpoint muss // dieselbe Grenze ziehen. if (!$user->hasRight('mahnung', 'write')) { httpExitError(403, $langs->trans('MahnungNichtBerechtigtWrite')); } $factureIds = GETPOST('facture_ids', 'array:int'); $factureIds = array_values(array_filter(array_unique(array_map('intval', $factureIds)), function ($v) { return $v > 0; })); if (empty($factureIds)) { httpExitError(400, $langs->trans('MahnungKeineRechnungenAusgewaehlt')); } $service = new MahnungVorschlag($db); // Bewusst VOR der Stufen-Validierung: liefert die Stufenkonfiguration einen // Fehler, soll dieser gemeldet werden und nicht "Stufe ungültig". $vorschlaege = mahnungVorschlagsIndex($service); if ($vorschlaege === false) { httpExitError(500, mahnungVorschlagFehlertext($service)); } // Zielstufe erzwingen? 0 ist gültig (Zahlungserinnerung), '' heißt "nicht gesetzt". $forceStufe = mahnungGetForceStufe($service); if ($forceStufe === MAHNUNG_STUFE_UNGUELTIG) { httpExitError(400, $langs->trans('MahnungStufeUngueltig')); } $basiszins = (float) getDolGlobalString('MAHNUNG_BASISZINS', '1.27'); $paths = array(); $skippedErinnerung = 0; foreach ($factureIds as $fid) { if (!isset($vorschlaege[$fid])) { continue; } $row = $vorschlaege[$fid]; $stufeNr = ($forceStufe !== MAHNUNG_STUFE_NICHT_GESETZT) ? $forceStufe : (isset($row['vorgeschlagene_stufe']) ? (int) $row['vorgeschlagene_stufe'] : null); if ($stufeNr === null) { continue; } $stufe = $service->getStufe($stufeNr); if ($stufe === null) { continue; } // Zahlungserinnerungen gehören nicht in den Sammelbrief — sie erzeugen kein // Mahn-PDF und werden per E-Mail mit der Original-Rechnung verschickt. if ($stufe->istErinnerung()) { $skippedErinnerung++; continue; } $mahnung = mahnungBaueVorgang($db, $row, $stufe, $basiszins, Mahnung::VERSAND_DRUCK); if ($mahnung->create($user) <= 0) { dol_syslog('Mahnung Sammelbrief: Anlage für Rechnung #'.((int) $fid).' fehlgeschlagen: '.$mahnung->error, LOG_ERR); continue; } // Dokumentenmodell-System statt eigener PDF-Klasse. Bewusst hart auf das // PDF-Modell: ein ODT-Dokument ließe sich nicht in den Sammelbrief mischen. $docResult = $mahnung->generateDocument('standard_mahnung', $langs); if ($docResult <= 0) { dol_syslog('Mahnung Sammelbrief: Dokument für '.$mahnung->ref.' fehlgeschlagen: '.$mahnung->error, LOG_ERR); continue; } // pdf_path setzt das Dokumentenmodell beim Schreiben, last_main_doc die // Dolibarr-Basis. Beides prüfen, damit ein Modellwechsel nichts bricht. $pdfPath = ''; if (!empty($mahnung->pdf_path)) { $pdfPath = (string) $mahnung->pdf_path; } elseif (!empty($mahnung->last_main_doc)) { $pdfPath = (string) $mahnung->last_main_doc; } if ($pdfPath === '' || !file_exists($pdfPath)) { dol_syslog('Mahnung Sammelbrief: PDF zu '.$mahnung->ref.' nicht auffindbar ('.$pdfPath.')', LOG_ERR); continue; } $paths[] = $pdfPath; } if (empty($paths)) { if ($skippedErinnerung > 0) { httpExitError(400, $langs->trans('MahnungSammelbriefNurErinnerungen')); } httpExitError(500, $langs->trans('MahnungSammelbriefKeinePdfs')); } // Alle Einzel-PDFs zu einem Dokument zusammenfassen (TCPDI). $absOut = mahnungBaueSammelbriefPdf($paths); if ($absOut === null || !file_exists($absOut)) { httpExitError(500, $langs->trans('MahnungSammelbriefFehler')); } while (ob_get_level() > 0) { ob_end_clean(); } header('Content-Type: application/pdf'); header('Content-Disposition: attachment; filename="sammelbrief-'.dol_print_date(dol_now(), 'dayxcard').'.pdf"'); header('Content-Length: '.filesize($absOut)); readfile($absOut); @unlink($absOut); exit; // ---------------------------------------------------------------- /** * Konkateniert mehrere PDF-Dateien zu einer temporären Datei. Gibt deren * absoluten Pfad zurück oder null bei Fehler. * * Der Rückgabewert ist IMMER eine Wegwerf-Kopie im Temp-Verzeichnis — der * Aufrufer löscht sie nach dem Download, und dabei darf niemals das Original-PDF * einer Mahnung erwischt werden. * * @param string[] $paths Absolute Pfade der Einzel-PDFs * @return string|null */ function mahnungBaueSammelbriefPdf(array $paths) { $out = sys_get_temp_dir().'/mahnung-sammelbrief-'.uniqid('', true).'.pdf'; if (!class_exists('TCPDI')) { // Dolibarr liefert TCPDI über tcpdf/tcpdi.php aus $tcpdiPath = DOL_DOCUMENT_ROOT.'/includes/tcpdf/tcpdi.php'; if (file_exists($tcpdiPath)) { require_once $tcpdiPath; } } if (!class_exists('TCPDI')) { dol_syslog('Mahnung Sammelbrief: TCPDI-Klasse nicht verfügbar — nur erstes PDF wird zurückgeliefert', LOG_WARNING); $first = reset($paths); if (empty($first) || !file_exists($first)) { return null; } return @copy($first, $out) ? $out : null; } $pdf = new TCPDI(); $pdf->setPrintHeader(false); $pdf->setPrintFooter(false); foreach ($paths as $src) { if (!file_exists($src)) { continue; } $pageCount = $pdf->setSourceFile($src); for ($p = 1; $p <= $pageCount; $p++) { $tplIdx = $pdf->importPage($p); $size = $pdf->getTemplateSize($tplIdx); $pdf->AddPage($size['orientation'] ?? 'P', array($size['width'], $size['height'])); $pdf->useTemplate($tplIdx); } } $pdf->Output($out, 'F'); if (!file_exists($out) || filesize($out) <= 0) { return null; } return $out; } /** * Bricht mit HTTP-Status und Klartext-Meldung ab. * * @param int $code * @param string $message * @return void */ function httpExitError($code, $message) { while (ob_get_level() > 0) { ob_end_clean(); } http_response_code($code); header('Content-Type: text/plain; charset=utf-8'); echo $message; exit; }