* * 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/class/mahnungpdf.class.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. */ 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 { /** @var DoliDB */ public $db; /** @var string */ public $error = ''; /** * @param DoliDB $db */ public function __construct($db) { $this->db = $db; } /** * 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 */ public function generate(Mahnung $mahnung, $user) { global $conf, $langs, $mysoc; $langs->loadLangs(array('main', 'bills', 'companies', 'mahnung@mahnung')); // Original-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; } $societe = new Societe($this->db); if ($societe->fetch((int) $mahnung->fk_soc) <= 0) { $this->error = 'Kunde '.((int) $mahnung->fk_soc).' nicht ladbar.'; return false; } $stufeObj = new MahnungStufe($this->db); if ($stufeObj->fetchByStufe((int) $mahnung->stufe) <= 0) { $this->error = 'Mahnstufe '.((int) $mahnung->stufe).' nicht konfiguriert.'; return false; } // Ziel-Verzeichnis im Doc-Ordner der Rechnung $dirOutput = $this->getOutputDir($facture); if (!dol_mkdir($dirOutput)) { $this->error = 'Kann Verzeichnis nicht anlegen: '.$dirOutput; return false; } $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); $pdf->SetAutoPageBreak(true, 25); $pdf->SetFont(pdf_getPDFFont($langs), '', $default_font_size); $pdf->SetTitle($langs->trans('MahnungStufe').' '.((int) $mahnung->stufe).' — '.$facture->ref); $pdf->SetSubject($langs->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->renderFooter($pdf); $pdf->Output($absPath, 'F'); // Pfad in DB persistieren $mahnung->pdf_path = $absPath; $mahnung->update($user); return $absPath; } /** * 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 */ private function renderHeader($pdf, $facture, $societe, $mahnung, $stufe) { global $langs, $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(); if (!empty($societe->name)) { $lines[] = $societe->name; } if (!empty($societe->name_alias)) { $lines[] = $societe->name_alias; } if (!empty($societe->address)) { $lines[] = $societe->address; } $ortzeile = trim(($societe->zip ?? '').' '.($societe->town ?? '')); if (!empty($ortzeile)) { $lines[] = $ortzeile; } foreach ($lines as $line) { $pdf->Cell(85, 5, $line, 0, 1, 'L'); $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->SetX(125); $pdf->Cell(60, 4, $langs->trans('MahnungRef').': '.$mahnung->ref, 0, 1, 'L'); $pdf->SetX(125); $pdf->Cell(60, 4, $langs->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; $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 */ private function renderBody($pdf, $facture, $societe, $mahnung, $stufe) { global $langs, $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->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); } $pdf->SetX(25); $pdf->MultiCell(160, 5, $intro, 0, 'L'); $pdf->Ln(3); // 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->SetFont('helvetica', '', 10); $pdf->SetX(25); $gezahlt = (float) $facture->total_ttc - (float) $mahnung->betrag_offen; $pdf->Cell(40, 6, $facture->ref, 0, 0, 'L'); $pdf->Cell(30, 6, dol_print_date($facture->date, 'day'), 0, 0, 'L'); $pdf->Cell(30, 6, price((float) $facture->total_ttc).' EUR', 0, 0, 'R'); $pdf->Cell(30, 6, price($gezahlt).' EUR', 0, 0, 'R'); $pdf->Cell(30, 6, price((float) $mahnung->betrag_offen).' EUR', 0, 1, 'R'); $pdf->Ln(3); // Gebuehrenblock $pdf->SetX(25); $pdf->SetFont('helvetica', '', 10); $pdf->Cell(130, 6, $langs->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(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(30, 6, price((float) $mahnung->pauschale_b2b).' EUR', 0, 1, 'R'); } if ((float) $mahnung->verzugszinsen > 0) { $pdf->SetX(25); $basis = $mahnung->basiszins_snapshot !== null ? (float) $mahnung->basiszins_snapshot : 0.0; $auf = $mahnung->customertype === Mahnung::KUNDENTYP_B2B ? (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(30, 6, price((float) $mahnung->verzugszinsen).' EUR', 0, 1, 'R'); } // Gesamtsumme $pdf->Ln(1); $pdf->SetX(25); $pdf->SetFont('helvetica', 'B', 11); $pdf->Cell(130, 7, $langs->trans('MahnungSumme'), 'T', 0, 'L'); $pdf->Cell(30, 7, price((float) $mahnung->summe_mahnung).' EUR', 'T', 1, 'R'); $pdf->Ln(5); // Neue Frist $pdf->SetX(25); $pdf->SetFont('helvetica', '', 11); $frist = $mahnung->date_lim_reglement_neu ? dol_print_date($mahnung->date_lim_reglement_neu, 'day') : ''; $fristText = empty($frist) ? 'Wir bitten Sie um umgehende Begleichung.' : 'Wir bitten Sie, den ausstehenden Betrag bis spaetestens '.$frist.' auf das unten genannte Konto zu ueberweisen.'; $pdf->MultiCell(160, 5, $fristText, 0, 'L'); $pdf->Ln(4); $pdf->SetX(25); $pdf->Cell(0, 5, 'Mit freundlichen Gruessen', 0, 1, 'L'); $pdf->SetX(25); $pdf->Cell(0, 5, (string) $mysoc->name, 0, 1, 'L'); } /** * Fusszeile mit Bankverbindung + Firmen-Footer. * * @param TCPDF $pdf * @return void */ private function renderFooter($pdf) { global $mysoc; $pdf->SetY(-30); $pdf->SetFont('helvetica', 'I', 8); $lines = array(); $lines[] = trim(($mysoc->name ?? '').' · '.($mysoc->address ?? '').' · '.($mysoc->zip ?? '').' '.($mysoc->town ?? '')); if (!empty($mysoc->email)) { $lines[] = 'E-Mail: '.$mysoc->email; } if (!empty($mysoc->phone)) { $lines[] = 'Tel: '.$mysoc->phone; } // Bankverbindung aus Standard-Bankaccount $bankAccount = $this->getDefaultBankLine(); if (!empty($bankAccount)) { $lines[] = $bankAccount; } foreach ($lines as $l) { $pdf->Cell(0, 4, $l, 0, 1, 'C'); } } /** * Standard-Bankkonto in einer Zeile (Bank · IBAN · BIC). * * @return string */ private function getDefaultBankLine() { $sql = "SELECT label, iban_prefix as iban, bic FROM ".MAIN_DB_PREFIX."bank_account"; $sql .= " WHERE clos = 0 AND default_rib = 1"; $sql .= " LIMIT 1"; $resql = $this->db->query($sql); if (!$resql || !$this->db->num_rows($resql)) { return ''; } $obj = $this->db->fetch_object($resql); $this->db->free($resql); $parts = array_filter(array($obj->label, $obj->iban ? 'IBAN '.$obj->iban : '', $obj->bic ? 'BIC '.$obj->bic : '')); return implode(' · ', $parts); } /** * Default-Intro je Stufe (wenn Setup leer ist). * * @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.'; } } /** * Ziel-Verzeichnis: documents/facture/{ref}/ * * @param Facture $facture * @return string */ private function getOutputDir($facture) { global $conf; $documentDir = !empty($conf->facture->multidir_output[$facture->entity]) ? $conf->facture->multidir_output[$facture->entity] : $conf->facture->dir_output; return rtrim($documentDir, '/').'/'.dol_sanitizeFileName($facture->ref); } }