* * 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. * Basiert auf box_factures_imp.php, erweitert um Mahnstufe-Spalte. */ 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'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/lib/mahnung_ui.lib.php'; /** * Widget: Aelteste offene Kundenrechnungen mit Mahnstufe-Badge. */ class box_mahnung_offen extends ModeleBoxes { public $boxcode = "mahnungoffenerechnungen"; public $boximg = "object_bill"; public $boxlabel = "MahnungBoxOffeneRechnungen"; public $depends = array("facture", "mahnung"); /** @var array Zahlprognose je socid — 1 Query pro Kunde/Request (Cache) */ private static $prognoseCache = array(); /** @var int[]|null Stufennummern mit ist_erinnerung = 1 — 1 Query pro Request (Cache) */ private static $erinnerungsStufenCache = null; /** * @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); // Kopf mit Titel; die echte Anzahl offener Rechnungen wird nach der Query // als Badge ergaenzt (siehe unten). Fuer die fruehen Return-Pfade // (fehlende Rechte / SQL-Fehler) bleibt es beim reinen Titel. $textHead = $langs->trans("MahnungBoxOffeneRechnungen"); $this->info_box_head = array( 'text' => $textHead, 'limit' => dol_strlen($textHead), ); if (!$user->hasRight('facture', 'lire')) { $this->info_box_contents[0][0] = array( 'td' => 'class="nohover left"', 'text' => ''.$langs->trans("ReadPermissionNotAllowed").'' ); return; } // Query wie box_factures_imp, plus Mahnstufe per Subquery $sql = "SELECT s.rowid as socid, s.nom as name, s.code_client, s.client,"; $sql .= " s.logo, s.email, s.entity,"; $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 .= " SUM(pf.amount) as am,"; // Letzte aktive Mahnstufe. Der Zweit-Sortierschlüssel rowid DESC ist Pflicht: // ohne ihn könnten die drei Subqueries bei zwei Mahnvorgängen derselben Stufe // unterschiedliche Zeilen treffen (Badge-Datum und Kartenlink aus verschiedenen // Mahnungen). Stufe 0 (Zahlungserinnerung) ist eine gültige Stufe. $sql .= " (SELECT m2.stufe FROM ".MAIN_DB_PREFIX."mahnung_mahnung as m2"; $sql .= " WHERE m2.fk_facture = f.rowid AND m2.status != ".((int) Mahnung::STATUS_STORNIERT); $sql .= " ORDER BY m2.stufe DESC, m2.rowid 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 != ".((int) Mahnung::STATUS_STORNIERT); $sql .= " ORDER BY m3.stufe DESC, m3.rowid DESC LIMIT 1) as mahndatum,"; $sql .= " (SELECT m4.rowid FROM ".MAIN_DB_PREFIX."mahnung_mahnung as m4"; $sql .= " WHERE m4.fk_facture = f.rowid AND m4.status != ".((int) Mahnung::STATUS_STORNIERT); $sql .= " ORDER BY m4.stufe DESC, m4.rowid DESC LIMIT 1) as mahnid"; $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"; if (empty($user->socid) && !$user->hasRight('societe', 'client', 'voir')) { $sql .= " AND s.rowid = (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_soc = s.rowid AND sc.fk_user = ".((int) $user->id)." LIMIT 1)"; } if ($user->socid) { $sql .= " AND s.rowid = ".((int) $user->socid); } $sql .= " GROUP BY s.rowid, s.nom, s.code_client, s.client, s.logo, s.email, s.entity,"; $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"; // Kein plimit: alle offenen Rechnungen laden, damit der Zaehler im Kopf // stimmt. Die Anzeige wird bei Bedarf ueber MAHNUNG_BOX_MAXLINES begrenzt. $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)'; // Echte Anzahl offener Rechnungen als Badge im Kopf (verlinkt auf die Liste). // $num entspricht dank GROUP BY genau einer Zeile je offener Rechnung. $this->info_box_head['text'] = $textHead .''.((int) $num).''; // Damit das Widget auch ohne offene Rechnungen sichtbar bleibt: // leeres info_box_contents würde ModeleBoxes::showBox nichts rendern lassen. if ($num == 0) { $this->info_box_contents[0][] = array( 'td' => 'class="center opacitymedium" colspan="7"', 'text' => $langs->trans("MahnungBoxKeineOffenenRechnungen"), ); } // Anzeige-Limit aus dem Setup (0 = alle offenen Rechnungen anzeigen). // Es werden immer alle geladen (fuer den korrekten Zaehler im Kopf), // aber nur $renderLimit Zeilen gerendert. $boxmax = getDolGlobalInt('MAHNUNG_BOX_MAXLINES', 0); $renderLimit = ($boxmax > 0) ? min($num, $boxmax) : $num; // Stufen-Konfiguration einmal laden — daraus kommt die Kennzeichnung // "kostenlose Zahlungserinnerung" (Flag ist_erinnerung) für das Badge. $erinnerungsStufen = $this->getErinnerungsStufen(); while ($line < $renderLimit) { $objp = $this->db->fetch_object($result); $datelimit = $this->db->jdate($objp->datelimit); $dateinvoice = $this->db->jdate($objp->date); $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 (mit Link zur Mahnung) oder Strich für keine Mahnung. // KEIN empty(): die Zahlungserinnerung hat per Default die Stufennummer 0, // empty('0') wäre true — die Zeile würde fälschlich als "keine Mahnung" // (Strich, kein Link zur Karte) erscheinen. Nur NULL/'' bedeutet // "zu dieser Rechnung existiert kein Mahnvorgang". $mahnCell = ''; if ($objp->mahnstufe !== null && $objp->mahnstufe !== '') { $stufe = (int) $objp->mahnstufe; // Ob es eine kostenlose Zahlungserinnerung ist, entscheidet ausschließlich // das Flag ist_erinnerung der Stufe — NICHT die Stufennummer. $istErinnerung = in_array($stufe, $erinnerungsStufen, true); $mahnDatum = $objp->mahndatum ? dol_print_date($this->db->jdate($objp->mahndatum), 'day') : ''; // Farbskala kommt aus lib/mahnung_ui.lib.php — dieselbe Quelle wie auf // der Mahnungskarte und in der Vorschlagsliste. $color = mahnungStufeFarbe($stufe, $istErinnerung); if ($istErinnerung) { $label = $langs->trans('MahnungBoxErinnerung'); $tooltip = $mahnDatum ? $langs->trans('MahnungBoxErinnerungVom', $mahnDatum) : $langs->trans('MahnungIstErinnerung'); } else { // Im Widget bewusst nur "Stufe N" statt der ausgeschriebenen // Bezeichnung: die Spalte ist schmal, der Klick führt zur Karte. $label = $langs->trans('MahnungBoxStufe', $stufe); $tooltip = $mahnDatum ? $langs->trans('MahnungBoxStufeVom', $stufe, $mahnDatum) : $label; } $badge = ''.dol_escape_htmltag($label).''; if ((int) $objp->mahnid > 0) { $mahnCell = ''.$badge.''; } else { $mahnCell = $badge; } } // 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: Fälligkeitsdatum $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: Voraussichtliche Zahlung (Prognose aus dem Zahlungsverhalten) $this->info_box_contents[$line][] = array( 'td' => 'class="center nowraponall"', 'text' => $this->buildPrognoseCell((int) $objp->socid, $datelimit, $dateinvoice), 'asis' => 1, ); // Spalte 6: Mahnstufe $this->info_box_contents[$line][] = array( 'td' => 'class="center nowraponall"', 'text' => $mahnCell, 'asis' => 1, ); // Spalte 7: Status (rechts am Rand, schmal) $this->info_box_contents[$line][] = array( 'td' => 'class="nowraponall right" width="16"', 'text' => $facturestatic->LibStatut($objp->paye, $objp->status, 3, $objp->am, $objp->type), ); $line++; } // Mehr offene Rechnungen vorhanden als angezeigt: "..."-Zeile als Verweis if ($boxmax > 0 && $num > $boxmax) { $this->info_box_contents[$line][] = array('td' => 'colspan="7"', 'text' => '...'); $line++; } if ($num > 0) { // Summe (wie Original: separate Query ohne LIMIT). // Brutto direkt aus f.total_ttc der Rechnung, NICHT aus total_ht // hochgerechnet — so bleiben Sonderregelungen (Reverse-Charge §13b, // Steuerbefreiung, Kleinunternehmer §19 UStG) korrekt. $sql2 = "SELECT SUM(f.total_ht) as total_ht, SUM(f.total_ttc) as total_ttc"; $sql2 .= " FROM ".MAIN_DB_PREFIX."facture as f"; $sql2 .= " INNER JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = f.fk_soc"; $sql2 .= " WHERE f.entity IN (".getEntity('invoice').")"; $sql2 .= " AND f.paye = 0 AND f.fk_statut = 1"; if ($user->socid) { $sql2 .= " AND s.rowid = ".((int) $user->socid); } $resTotal = $this->db->query($sql2); $totalHt = 0; $totalTtc = 0; if ($resTotal) { $objTotal = $this->db->fetch_object($resTotal); $totalHt = (float) $objTotal->total_ht; $totalTtc = (float) $objTotal->total_ttc; $this->db->free($resTotal); } $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' => ' ', ); // Netto (wie bisher) + Brutto darunter, Brutto aus f.total_ttc der // Rechnung (Sonderregelungen wie §13b/§19 UStG bleiben korrekt). $this->info_box_contents[$line][] = array( 'td' => 'class="nowraponall right liste_total"', 'text' => ''.$langs->trans("MahnungBoxNetto").''.price($totalHt, 0, $langs, 0, -1, -1, $conf->currency) .'
'.$langs->trans("MahnungBoxBrutto").''.price($totalTtc, 0, $langs, 0, -1, -1, $conf->currency), 'asis' => 1, ); // Fälligkeit-Spalte (leer) $this->info_box_contents[$line][] = array( 'td' => 'class="liste_total"', 'text' => ' ', ); // Vsl.-Zahlung-Spalte (leer) $this->info_box_contents[$line][] = array( 'td' => 'class="liste_total"', 'text' => ' ', ); $this->info_box_contents[$line][] = array( 'td' => 'class="liste_total"', 'text' => ' ', ); $this->info_box_contents[$line][] = array( 'td' => 'class="liste_total"', 'text' => ' ', ); } $this->db->free($result); } /** * Stufennummern der Entity, die als kostenlose Zahlungserinnerung konfiguriert * sind (Flag ist_erinnerung = 1). Maßgeblich ist das Flag, NICHT die Nummer — * Stufennummern sind frei wählbar. Wird pro Request genau einmal gelesen * (kleine Tabelle, wenige Zeilen). * * Bewusst "SELECT *": auf Installationen, auf denen die Schema-Migration * (Setup-Seite einmal aufrufen) noch nicht lief, fehlt die Spalte * ist_erinnerung — ein explizites Feld-SELECT würde die Query und damit das * ganze Widget in einen SQL-Fehler laufen lassen. Dort bleibt die Liste leer * und das Widget zeigt wie bisher nur die Stufennummer. * Auch INAKTIVE Stufen werden geladen, damit Bestandsmahnungen auf einer * später deaktivierten Stufe weiterhin richtig beschriftet werden. * * @return int[] Stufennummern mit ist_erinnerung = 1 */ private function getErinnerungsStufen() { if (self::$erinnerungsStufenCache !== null) { return self::$erinnerungsStufenCache; } self::$erinnerungsStufenCache = array(); $sql = "SELECT * FROM ".MAIN_DB_PREFIX."mahnung_stufe"; $sql .= " WHERE entity IN (".getEntity('mahnung').")"; $resql = $this->db->query($sql); if (!$resql) { // Kein harter Fehler: das Widget fällt dann auf die reine Stufennummer zurück dol_syslog("box_mahnung_offen::getErinnerungsStufen SQL-Fehler: ".$this->db->lasterror(), LOG_ERR); return self::$erinnerungsStufenCache; } while ($obj = $this->db->fetch_object($resql)) { // isset-Guard: Spalte fehlt, solange die Schema-Migration noch nicht lief if (!empty($obj->ist_erinnerung)) { self::$erinnerungsStufenCache[] = (int) $obj->stufe; } } $this->db->free($resql); return self::$erinnerungsStufenCache; } /** * Zahlungsverhalten eines Kunden aus seinen bezahlten Rechnungen. * * Rechnung IDENTISCH zu BuchhaltungsWidget::getPaymentStatistics() (KB #886), * damit die Ampel hier zur "Zahlungsverhalten"-Box auf der Kundenkarte passt: * diff = Ø(Zahlung − Rechnungsdatum) − Ø(Fälligkeit − Rechnungsdatum) * = durchschnittliche Tage NACH Fälligkeit (negativ = vorher gezahlt). * Bewusst nur Standard/Ersatz/Situation (type IN 0,1,5), voll bezahlt * (fk_statut=2 + paye=1), mit Fälligkeit. Selbst-enthaltene Kopie — * KEINE Laufzeit-Abhängigkeit auf BuchhaltungsWidget. * * @param int $socid * @return array{count:int,diff:float}|null null bei Fehler / ohne Historie */ private function getZahlprognose($socid) { $socid = (int) $socid; if ($socid <= 0) { return null; } if (array_key_exists($socid, self::$prognoseCache)) { return self::$prognoseCache[$socid]; } $sql = "SELECT COUNT(*) as invoice_count,"; $sql .= " AVG(DATEDIFF(sub.pay_date, f.datef)) as avg_payment_days,"; $sql .= " AVG(DATEDIFF(f.date_lim_reglement, f.datef)) as avg_due_days"; $sql .= " FROM ".MAIN_DB_PREFIX."facture as f"; $sql .= " INNER JOIN ("; $sql .= " SELECT pf.fk_facture, MAX(p.datep) as pay_date"; $sql .= " FROM ".MAIN_DB_PREFIX."paiement_facture as pf"; $sql .= " INNER JOIN ".MAIN_DB_PREFIX."paiement as p ON p.rowid = pf.fk_paiement"; $sql .= " GROUP BY pf.fk_facture"; $sql .= " ) as sub ON sub.fk_facture = f.rowid"; $sql .= " WHERE f.fk_soc = ".$socid; $sql .= " AND f.fk_statut = 2 AND f.paye = 1"; $sql .= " AND f.type IN (0, 1, 5)"; $sql .= " AND f.date_lim_reglement IS NOT NULL"; $sql .= " AND f.entity IN (".getEntity('invoice').")"; $out = null; $res = $this->db->query($sql); if ($res) { $o = $this->db->fetch_object($res); if ($o && (int) $o->invoice_count > 0) { $out = array( 'count' => (int) $o->invoice_count, // avg_pay = Ø Tage nach Rechnungseingang (intuitive "Days to Pay"-Zahl). 'avg_pay' => (float) $o->avg_payment_days, // diff = Ø Tage nach Fälligkeit (nur für die Ampel-Bewertung, KB #886). 'diff' => (float) $o->avg_payment_days - (float) $o->avg_due_days, ); } $this->db->free($res); } else { dol_syslog("box_mahnung_offen::getZahlprognose SQL-Fehler: ".$this->db->lasterror(), LOG_ERR); } self::$prognoseCache[$socid] = $out; return $out; } /** * Bewertungsstufe zu einem diff-Wert (Ø Tage nach Fälligkeit). * Schwellen, Farben und Icons IDENTISCH zu BuchhaltungsWidget (KB #886). * * @param float $diff * @return array{icon:string,color:string,labelkey:string} */ private function prognoseRating($diff) { if ($diff <= -5) { return array('icon' => '★', 'color' => '#28a745', 'labelkey' => 'MahnungProgVorbildlich'); // ★ } elseif ($diff <= 0) { return array('icon' => '✓', 'color' => '#28a745', 'labelkey' => 'MahnungProgPuenktlich'); // ✓ } elseif ($diff <= 7) { return array('icon' => '⚠', 'color' => '#ffc107', 'labelkey' => 'MahnungProgSpaetzahler'); // ⚠ } elseif ($diff <= 14) { return array('icon' => '⏱', 'color' => '#fd7e14', 'labelkey' => 'MahnungProgVerspaetet'); // ⏱ } return array('icon' => '✗', 'color' => '#dc3545', 'labelkey' => 'MahnungProgProblematisch'); // ✗ } /** * Baut die HTML-Zelle "Vsl. Zahlung": Ampel-Icon + Prognosedatum + Kurzstatistik. * * Prognose = Rechnungsdatum (datef) + Ø Tage nach Rechnungseingang (avg_pay) — * die intuitive "Days to Pay"-Kennzahl. Verstrichenes Prognosedatum bei noch * offener Rechnung → Datum wird rot eingefärbt (kompakt, Erklärung im Tooltip), * KEIN zusätzlicher Text (würde die Widget-Spalte zu breit machen). * Ampel-Icon bleibt auf der Fälligkeits-Skala (diff), damit es zur * "Zahlungsverhalten"-Box der Kundenkarte passt. * * @param int $socid * @param int $datelimit Fälligkeit als Unix-Timestamp (jdate) — Fallback-Anker * @param int $dateinvoice Rechnungsdatum (datef) als Unix-Timestamp (jdate) * @return string HTML (wird 'asis' gerendert) */ private function buildPrognoseCell($socid, $datelimit, $dateinvoice) { global $langs; $prog = $this->getZahlprognose($socid); // Default 1 = Parität zur "Zahlungsverhalten"-Box (BuchhaltungsWidget, KB #886), // die ab der 1. bezahlten Rechnung bewertet. Höher setzen, wenn Einzelrechnungs- // Prognosen als zu unsicher empfunden werden. $minN = getDolGlobalInt('MAHNUNG_PROGNOSE_MIN_N', 1); // transnoentities() liefert rohes UTF-8 (kein ü) — sonst würde das // nachfolgende dol_escape_htmltag() das & doppelt kodieren. // Zu wenig Historie (oder gar keine) → keine belastbare Prognose if ($prog === null || $prog['count'] < $minN) { return '' .'○ '.dol_escape_htmltag($langs->transnoentities('MahnungProgKeineHistorie')).''; } $rating = $this->prognoseRating($prog['diff']); $label = $langs->transnoentities($rating['labelkey']); // Prognosedatum = Rechnungsdatum + Ø Tage nach Rechnungseingang. Verankerung am // Rechnungsdatum (statt Fälligkeit) ist intuitiv und vermeidet Prognosen VOR dem // Rechnungsdatum. Fallback auf Fälligkeit + diff, falls datef fehlt. if ((int) $dateinvoice > 0) { $progTs = (int) round((int) $dateinvoice + $prog['avg_pay'] * 86400); } else { $progTs = (int) round((int) $datelimit + $prog['diff'] * 86400); } $progDate = dol_print_date($progTs, 'day', 'tzuserrel'); $nachRe = sprintf('%.1f', $prog['avg_pay']); // Ø Tage nach Rechnungseingang (i.d.R. positiv) // Prognosedatum verstrichen, Rechnung noch offen → zahlt langsamer als sein Muster. // Kompakt: nur das Datum rot färben + Tooltip-Zusatz (kein Extra-Text in der Zelle). $dateStyle = ''; $tipExtra = ''; if ($progTs < dol_now()) { $dateStyle = ' style="color:#dc3545;"'; $tipExtra = ' — '.$langs->transnoentities('MahnungProgSpaeterAlsUeblich'); } // Voller Kontext im Tooltip (Parameter direkt an trans → macht das %s-sprintf selbst). $tooltip = $label.': '.$langs->transnoentities('MahnungProgTooltip', $progDate, $nachRe, (string) $prog['count']).$tipExtra; $html = ''; $html .= ''.$rating['icon'].' '; $html .= '~'.$progDate.''; $html .= '
Ø '.$nachRe.' ' .dol_escape_htmltag($langs->transnoentities('MahnungProgNachRechnung')).' · n='.((int) $prog['count']).''; $html .= ''; return $html; } /** * @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); } }