All checks were successful
Deploy mahnung / deploy (push) Successful in 13s
Fix nach Eddys Beobachtung: Kunde mit Historie auf der Kundenkarte (BuchhaltungsWidget bewertet ab n>=1) erschien im Widget als "keine Historie", weil MAHNUNG_PROGNOSE_MIN_N auf 3 stand. - MIN_N Default 3 -> 1: Paritaet zur Zahlungsverhalten-Box, die ebenfalls ab der 1. bezahlten Rechnung bewertet. Weiter per Konstante erhoehbar. - "(ueberfaellig)" -> "(spaeter als ueblich)": Das Flag greift wenn das Prognosedatum verstrichen und die Rechnung noch offen ist - das kann schon VOR der formellen Faelligkeit passieren (Fruehzahler). "ueberfaellig" war dafuer das falsche Wort. Neuer Lang-Key MahnungProgSpaeterAlsUeblich. - Prognosedatum + OE-Verzug jetzt aus dem EXAKTEN Mittelwert (Timestamp statt gerundeter Tage), Anzeige mit einer Nachkommastelle -> deckt sich 1:1 mit "Differenz" auf der Kundenkarte (z.B. -3.1 T -> Faelligkeit-3.1). Lokal gegen die Test-DB verifiziert (n=1/n=2 werden gezeigt, Flag nur bei verstrichener Prognose, Zukunfts-Faelligkeit ohne Flag, php -l sauber). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
461 lines
18 KiB
PHP
461 lines
18 KiB
PHP
<?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);
|
||
|
||
$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),
|
||
'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' => '<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' => ' ',
|
||
);
|
||
// 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);
|
||
}
|
||
|
||
/**
|
||
* 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,
|
||
'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.
|
||
* Prognosedatum = Fälligkeit + round(diff). Prognose bereits vorbei, Rechnung aber
|
||
* noch offen → Zusatz "(überfällig)" (Kunde zahlt schlechter als sonst).
|
||
*
|
||
* @param int $socid
|
||
* @param int $datelimit Fälligkeit als Unix-Timestamp (jdate)
|
||
* @return string HTML (wird 'asis' gerendert)
|
||
*/
|
||
private function buildPrognoseCell($socid, $datelimit)
|
||
{
|
||
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 '<span class="opacitymedium" title="'.dol_escape_htmltag($langs->transnoentities('MahnungProgKeineHistorieTip')).'">'
|
||
.'○ '.dol_escape_htmltag($langs->transnoentities('MahnungProgKeineHistorie')).'</span>';
|
||
}
|
||
|
||
$rating = $this->prognoseRating($prog['diff']);
|
||
$label = $langs->transnoentities($rating['labelkey']);
|
||
|
||
// Prognosedatum aus dem EXAKTEN Ø-Verzug (gleiche Zahl wie "Differenz" auf der
|
||
// Kundenkarte); Anzeige mit einer Nachkommastelle, damit z.B. −3.1 exakt passt.
|
||
$progTs = (int) round((int) $datelimit + $prog['diff'] * 86400);
|
||
$progDate = dol_print_date($progTs, 'day', 'tzuserrel');
|
||
$avg = sprintf('%+.1f', $prog['diff']); // Vorzeichen: + nach Fälligkeit, − davor
|
||
|
||
// Prognosedatum bereits verstrichen, Rechnung aber noch offen → der Kunde zahlt
|
||
// langsamer als sein eigenes Muster. NICHT dasselbe wie "überfällig" (kann schon
|
||
// vor der formellen Fälligkeit greifen) — daher eigene Formulierung.
|
||
$hint = '';
|
||
if ($progTs < dol_now()) {
|
||
$hint = ' <span style="color:#dc3545;">('.dol_escape_htmltag($langs->transnoentities('MahnungProgSpaeterAlsUeblich')).')</span>';
|
||
}
|
||
|
||
// Parameter direkt an trans übergeben (macht das %s-sprintf selbst).
|
||
$tooltip = $langs->transnoentities('MahnungProgTooltip', $progDate, $avg, (string) $prog['count']);
|
||
|
||
$html = '<span title="'.dol_escape_htmltag($tooltip).'">';
|
||
$html .= '<span style="color:'.$rating['color'].';">'.$rating['icon'].'</span> ~'.$progDate.$hint;
|
||
$html .= '<br><span class="opacitymedium small">'.dol_escape_htmltag($label)
|
||
.' · Ø '.$avg.' T · 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);
|
||
}
|
||
}
|