mahnung/core/boxes/box_mahnung_offen.php
Eduard Wisch fdd2b17ab4
All checks were successful
Deploy mahnung / deploy (push) Successful in 13s
Widget-Prognose: intuitive "Ø X T nach Rechnung" + rotes Datum statt Text [deploy]
Eddy-Feedback: Die Differenz-zur-Faelligkeit (z.B. -13.5 T) war unintuitiv
(Kopfrechnen noetig) und "(spaeter als ueblich)" hat die Widget-Spalte
gesprengt.

- Prognosedatum jetzt am RECHNUNGSDATUM verankert: datef + Oe Tage nach
  Rechnungseingang (avg_pay). Das ist die klassische Days-to-Pay-Kennzahl,
  vermeidet Prognosen vor dem Rechnungsdatum und ist direkt lesbar.
- Sichtbare Zahl = "Oe X T nach Rechnung" (positiv) statt Differenz zur
  Faelligkeit. Ampel-Icon bleibt auf der Faelligkeits-Skala (diff) -> weiter
  Paritaet zur Zahlungsverhalten-Box der Kundenkarte.
- "(spaeter als ueblich)" als Text entfernt: bei verstrichener Prognose
  wird stattdessen nur das DATUM rot gefaerbt (Zeile bleibt schmal),
  Erklaerung steckt im Tooltip.
- getZahlprognose liefert zusaetzlich avg_pay; buildPrognoseCell bekommt
  das Rechnungsdatum durchgereicht. Neuer Lang-Key MahnungProgNachRechnung,
  Tooltip umformuliert.

Lokal gegen die Test-DB verifiziert (Datum aus datef+avg_pay, Rot nur bei
verstrichener Prognose, Zukunfts-Rechnung ohne Rot, php -l sauber).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 11:48:08 +02:00

478 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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.
* 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';
/**
* 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<int,array|null> Zahlprognose je socid — 1 Query pro Kunde/Request (Cache) */
private static $prognoseCache = array();
/**
* @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' => '<span class="opacitymedium">'.$langs->trans("ReadPermissionNotAllowed").'</span>'
);
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
$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 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 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 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
.'<a class="paddingleft valignmiddle" href="'.DOL_URL_ROOT.'/compta/facture/list.php?search_status=1&sortfield=f.date_lim_reglement,f.ref&sortorder=ASC,ASC"'
.' title="'.dol_escape_htmltag($textHead).'"><span class="badge">'.((int) $num).'</span></a>';
// 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;
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
$mahnCell = '<span class="opacitymedium">—</span>';
if (!empty($objp->mahnstufe)) {
$stufe = (int) $objp->mahnstufe;
$colors = array(1 => '#4a90d9', 2 => '#e68a00', 3 => '#cc3333');
$color = $colors[$stufe] ?? '#666';
$label = $langs->trans('MahnungBoxStufe', $stufe);
$mahnDatum = $objp->mahndatum ? dol_print_date($this->db->jdate($objp->mahndatum), 'day') : '';
$tooltip = $mahnDatum ? $langs->trans('MahnungBoxStufeVom', $stufe, $mahnDatum) : $label;
$badge = '<span class="badge" style="background-color:'.$color.';color:#fff;font-size:0.75em;" title="'.dol_escape_htmltag($tooltip).'">'.$label.'</span>';
if (!empty($objp->mahnid)) {
$mahnCell = '<a href="'.DOL_URL_ROOT.'/custom/mahnung/card.php?id='.((int) $objp->mahnid).'">'.$badge.'</a>';
} 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' => '&nbsp;',
);
// 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' => '<span class="opacitymedium paddingright small">'.$langs->trans("MahnungBoxNetto").'</span>'.price($totalHt, 0, $langs, 0, -1, -1, $conf->currency)
.'<br><span class="opacitymedium paddingright small">'.$langs->trans("MahnungBoxBrutto").'</span>'.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' => '&nbsp;',
);
// Vsl.-Zahlung-Spalte (leer)
$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);
}
/**
* 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' => '&#9733;', 'color' => '#28a745', 'labelkey' => 'MahnungProgVorbildlich'); // ★
} elseif ($diff <= 0) {
return array('icon' => '&#10003;', 'color' => '#28a745', 'labelkey' => 'MahnungProgPuenktlich'); // ✓
} elseif ($diff <= 7) {
return array('icon' => '&#9888;', 'color' => '#ffc107', 'labelkey' => 'MahnungProgSpaetzahler'); // ⚠
} elseif ($diff <= 14) {
return array('icon' => '&#9201;', 'color' => '#fd7e14', 'labelkey' => 'MahnungProgVerspaetet'); // ⏱
}
return array('icon' => '&#10007;', '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 &uuml;) — 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 '<span class="opacitymedium" title="'.dol_escape_htmltag($langs->transnoentities('MahnungProgKeineHistorieTip')).'">'
.'&#9675; '.dol_escape_htmltag($langs->transnoentities('MahnungProgKeineHistorie')).'</span>';
}
$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 = '<span title="'.dol_escape_htmltag($tooltip).'">';
$html .= '<span style="color:'.$rating['color'].';">'.$rating['icon'].'</span> ';
$html .= '<span'.$dateStyle.'>~'.$progDate.'</span>';
$html .= '<br><span class="opacitymedium small">&#216; '.$nachRe.'&nbsp;'
.dol_escape_htmltag($langs->transnoentities('MahnungProgNachRechnung')).' &middot; n='.((int) $prog['count']).'</span>';
$html .= '</span>';
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);
}
}