* * 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. * * POST: * facture_ids[] Rechnungs-IDs * stufe (opt) Stufe erzwingen (sonst Vorschlag) * token CSRF * * Response: PDF-Download "sammelbrief-YYYYMMDD-N.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/class/mahnungpdf.class.php'; global $db, $user, $langs; $langs->loadLangs(array('mahnung@mahnung')); // CSRF $postedToken = GETPOST('token', 'alphanohtml'); if (empty($postedToken) || empty($_SESSION['newtoken']) || $postedToken !== $_SESSION['newtoken']) { while (ob_get_level() > 0) { ob_end_clean(); } httpExitError(403, $langs->trans('MahnungSammelbriefCsrfFehler')); } // Permission if (!$user->hasRight('mahnung', 'send') && !$user->hasRight('mahnung', 'write')) { while (ob_get_level() > 0) { ob_end_clean(); } httpExitError(403, $langs->trans('MahnungSammelbriefNichtBerechtigt')); } $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)) { while (ob_get_level() > 0) { ob_end_clean(); } httpExitError(400, $langs->trans('MahnungKeineRechnungenAusgewaehlt')); } $forceStufe = GETPOSTINT('stufe'); $forceStufe = ($forceStufe >= 1 && $forceStufe <= 3) ? $forceStufe : 0; $service = new MahnungVorschlag($db); $pdfGen = new MahnungPdf($db); $basiszins = (float) getDolGlobalString('MAHNUNG_BASISZINS', '1.27'); $paths = array(); foreach ($factureIds as $fid) { $rows = $service->getVorschlaege(); $row = null; foreach ($rows as $r) { if ((int) $r['facture_id'] === (int) $fid) { $row = $r; break; } } if ($row === null) { continue; } $stufeNr = $forceStufe ?: (int) $row['vorgeschlagene_stufe']; $stufe = $service->getStufe($stufeNr); if ($stufe === null) { continue; } $mahnung = new Mahnung($db); $mahnung->fk_facture = $fid; $mahnung->fk_soc = (int) $row['soc_id']; $mahnung->stufe = $stufeNr; $mahnung->date_mahnung = dol_now(); $mahnung->date_lim_reglement_alt = $row['facture_date_lim_reglement']; $mahnung->date_lim_reglement_neu = dol_time_plus_duree(dol_now(), (int) $stufe->neue_frist_tage, 'd'); $mahnung->betrag_offen = (float) $row['betrag_offen']; $mahnung->customertype = $row['kundentyp']; $mahnung->basiszins_snapshot = $basiszins; $mahnung->versandart = Mahnung::VERSAND_DRUCK; $mahnung->mahngebuehr = $stufe->getMahngebuehr($mahnung->customertype); if ($mahnung->customertype === Mahnung::KUNDENTYP_B2B && (int) $stufe->pauschale_b2b_einmalig === 1 && !pauschaleBereitsAngewendet($db, $fid)) { $mahnung->pauschale_b2b = (float) getDolGlobalString('MAHNUNG_PAUSCHALE_B2B', '40.00'); } $mahnung->verzugszinsen = Mahnung::berechneVerzugszinsen( $mahnung->betrag_offen, (int) $row['tage_verzug'], $mahnung->customertype, $basiszins, $stufe->getZinssatzOverride($mahnung->customertype) ); $mahnung->rechneSumme(); $mahnung->status = Mahnung::STATUS_ERSTELLT; if ($mahnung->create($user) <= 0) { continue; } $pdfPath = $pdfGen->generate($mahnung, $user); if ($pdfPath !== false) { $paths[] = $pdfPath; } } if (empty($paths)) { while (ob_get_level() > 0) { ob_end_clean(); } httpExitError(500, $langs->trans('MahnungSammelbriefKeinePdfs')); } // Wenn TCPDI verfügbar, Seiten aller PDFs in EIN Dokument importieren. // Andernfalls ZIP-Fallback würde sich anbieten — wir liefern stattdessen // eine PDF-Konkatenation via TCPDI (Bestandteil von tecnickcom/tc-lib-pdf // und Dolibarr-Tcpdi-Wrapper). $absOut = this_buildSammelbriefPdf($paths); if ($absOut === null || !file_exists($absOut)) { while (ob_get_level() > 0) { ob_end_clean(); } 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 Datei. Gibt absoluten Pfad zurück * oder null bei Fehler. * * @param string[] $paths * @return string|null */ function this_buildSammelbriefPdf(array $paths) { 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); return $paths[0] ?? null; } $out = sys_get_temp_dir().'/mahnung-sammelbrief-'.uniqid('', true).'.pdf'; $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'); return $out; } /** * Prüft, ob für eine Rechnung bereits §288-B2B-Pauschale gesetzt wurde. * * @param DoliDB $db * @param int $factureId * @return bool */ function pauschaleBereitsAngewendet($db, $factureId) { $sql = "SELECT 1 FROM ".MAIN_DB_PREFIX."mahnung_mahnung"; $sql .= " WHERE fk_facture = ".((int) $factureId); $sql .= " AND status NOT IN (".Mahnung::STATUS_STORNIERT.")"; $sql .= " AND pauschale_b2b > 0"; $sql .= " LIMIT 1"; $resql = $db->query($sql); if (!$resql) { return false; } $has = (bool) $db->num_rows($resql); $db->free($resql); return $has; } /** * @param int $code * @param string $message */ function httpExitError($code, $message) { http_response_code($code); header('Content-Type: text/plain; charset=utf-8'); echo $message; exit; }