mahnung/list.php
Eduard Wisch 10cf41a687
All checks were successful
Deploy mahnung / deploy (push) Successful in 14s
i18n: Alle Texte über $langs->trans() — ~100 neue Sprachschlüssel de_DE + en_US [deploy]
Umlaute in allen lang-Dateien korrigiert. Alle hardcodierten deutschen Strings
in 22 PHP-Dateien durch $langs->trans('Key') ersetzt. Neue Schlüssel für
Cron-Meldungen, Dokument-Aktionen, Bonität, Vorschlag-Status, Template-Vars u.a.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 16:25:50 +02:00

399 lines
16 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 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
/**
* \file mahnung/list.php
* \ingroup mahnung
* \brief Vorschlagsliste (mode=vorschlag) und Mahnvorgänge-Archiv (mode=archiv).
*/
$res = 0;
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
}
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
$tmp2 = realpath(__FILE__);
$i = strlen($tmp) - 1;
$j = strlen($tmp2) - 1;
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
$i--;
$j--;
}
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
}
if (!$res && file_exists("../main.inc.php")) {
$res = @include "../main.inc.php";
}
if (!$res && file_exists("../../main.inc.php")) {
$res = @include "../../main.inc.php";
}
if (!$res) {
die("Include of main fails");
}
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.'/core/class/html.form.class.php';
global $langs, $user, $db;
$langs->loadLangs(array('mahnung@mahnung', 'companies', 'bills'));
if (!$user->hasRight('mahnung', 'read')) {
accessforbidden();
}
$form = new Form($db);
$mode = GETPOST('mode', 'aZ09');
if ($mode !== 'archiv') {
$mode = 'vorschlag';
}
$filter = array();
$filter_stufe = GETPOST('filter_stufe', 'int');
if ($filter_stufe !== '' && $filter_stufe !== null) {
$filter['stufe'] = (int) $filter_stufe;
}
$filter_minverzug = GETPOST('filter_minverzug', 'int');
if ($filter_minverzug !== '' && $filter_minverzug !== null) {
$filter['min_tage_verzug'] = (int) $filter_minverzug;
}
$filter_socid = GETPOST('search_socid', 'int');
// select_company liefert -1 für "nichts ausgewählt" — nur positive IDs als Filter werten
if ((int) $filter_socid > 0) {
$filter['soc_id'] = (int) $filter_socid;
}
$filter_minbetrag = GETPOST('filter_minbetrag', 'alpha'); // Komma/Punkt zugelassen
$filter_minbetrag_num = ($filter_minbetrag !== '' && $filter_minbetrag !== null)
? (float) str_replace(',', '.', $filter_minbetrag)
: null;
if ($filter_minbetrag_num !== null) {
$filter['min_betrag'] = $filter_minbetrag_num;
}
$filter_kundentyp = GETPOST('filter_kundentyp', 'alpha'); // '', 'B2B', 'B2C'
if ($filter_kundentyp === 'B2B' || $filter_kundentyp === 'B2C') {
$filter['kundentyp'] = $filter_kundentyp;
}
llxHeader('', $langs->trans($mode === 'archiv' ? 'MahnungArchiv' : 'MahnungVorschlagsliste'));
print load_fiche_titre(
$langs->trans($mode === 'archiv' ? 'MahnungArchiv' : 'MahnungVorschlagsliste'),
'',
'fa-envelope-open-text'
);
// --- Filter-Form ---
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="mainmenu" value="billing">';
print '<input type="hidden" name="leftmenu" value="mahnung">';
print '<input type="hidden" name="mode" value="'.dol_escape_htmltag($mode).'">';
print '<table class="noborder centpercent"><tr class="liste_titre">';
print '<th>'.$langs->trans('MahnungStufe').'</th>';
print '<th>'.$langs->trans('MahnungTageVerzug').' (min)</th>';
print '<th>'.$langs->trans('MahnungBetragOffen').' (min)</th>';
print '<th>'.$langs->trans('MahnungKundentyp').'</th>';
print '<th>'.$langs->trans('MahnungKunde').'</th>';
print '<th></th></tr>';
print '<tr>';
// Mahnstufe
print '<td><select name="filter_stufe">';
print '<option value="">— '.$langs->trans('All').' —</option>';
foreach (array(1, 2, 3) as $st) {
print '<option value="'.$st.'"'.((string) $filter_stufe === (string) $st ? ' selected' : '').'>'.$st.'</option>';
}
print '</select></td>';
// Mindestverzug
print '<td><input type="number" name="filter_minverzug" value="'.dol_escape_htmltag((string) $filter_minverzug).'" size="4"></td>';
// Mindestbetrag (in EUR, Komma erlaubt)
print '<td><input type="text" name="filter_minbetrag" value="'.dol_escape_htmltag((string) $filter_minbetrag).'" size="6" placeholder="0,00"> €</td>';
// Kundentyp — Auto-Submit beim Wechsel + Kunden-Auswahl zurücksetzen, damit das
// Kunden-Dropdown direkt mit dem neuen Typ-Filter neu geladen wird (alte Auswahl
// könnte im neuen Filter gar nicht mehr enthalten sein).
print '<td><select name="filter_kundentyp" onchange="';
print 'var s=this.form.elements[\'search_socid\']; if(s){s.value=\'\';} this.form.submit();';
print '">';
print '<option value="">— '.$langs->trans('All').' —</option>';
print '<option value="B2B"'.($filter_kundentyp === 'B2B' ? ' selected' : '').'>B2B</option>';
print '<option value="B2C"'.($filter_kundentyp === 'B2C' ? ' selected' : '').'>B2C</option>';
print '</select></td>';
// Kunden-Auswahl: Dolibarr-Standard select_company (Ajax wenn COMPANY_USE_SEARCH_TO_SELECT,
// sonst klassisches Dropdown). htmlname='search_socid' bleibt = Backward-Kompatibilität
// zu Direkt-Links (?search_socid=74) von der Kundenkarte. Wenn Kundentyp-Filter
// gesetzt ist, schränken wir die Dropdown-Liste passend ein.
//
// WICHTIG: select_thirdparty_list erwartet $filter im Universal-Search-Criteria-Format
// (siehe forgeSQLFromUniversalSearchCriteria), NICHT plain SQL.
// Syntax: (feld:operator:wert) mit AND/OR; Operatoren: =, !=, <, >, like, is, isnot, in, notin.
$socFilter = '';
if ($filter_kundentyp === 'B2B') {
$socFilter = "(s.tva_intra:isnot:NULL) AND (s.tva_intra:!=:'')";
} elseif ($filter_kundentyp === 'B2C') {
$socFilter = "(s.tva_intra:is:NULL) OR (s.tva_intra:=:'')";
}
print '<td>';
print $form->select_company(
$filter_socid, // selected
'search_socid', // htmlname (Hidden-Input-Name)
$socFilter, // filter (SQL-Condition, von Dolibarr in WHERE eingebunden)
'SelectThirdParty', // showempty (Translation-Key)
0, 0, array(), 0, 'minwidth250'
);
print '</td>';
print '<td><input type="submit" class="button" value="'.$langs->trans('Search').'"></td>';
print '</tr></table>';
print '</form><br>';
if ($mode === 'vorschlag') {
renderVorschlagsliste($db, $filter);
} else {
renderArchiv($db, $filter);
}
llxFooter();
$db->close();
/**
* Rendert die Vorschlagsliste auf Basis von MahnungVorschlag.
*
* @param DoliDB $db
* @param array $filter
* @return void
*/
function renderVorschlagsliste($db, $filter)
{
global $langs, $user;
$service = new MahnungVorschlag($db);
$rows = $service->getVorschlaege($filter);
$skipped = $service->getUebersprungeneRechnungen($filter);
if (empty($rows) && empty($skipped)) {
print '<div class="info">'.$langs->trans('MahnungKeineUeberfaelligen').'</div>';
return;
}
if (empty($rows)) {
print '<div class="info">'.$langs->trans('MahnungKeineUeberfaelligen').'</div>';
renderUebersprungeneTabelle($skipped);
return;
}
$canWrite = $user->hasRight('mahnung', 'write');
print '<form method="POST" action="'.DOL_URL_ROOT.'/custom/mahnung/ajax/createmahnung.php" id="formMahnung">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
if ($canWrite) {
print '<th><input type="checkbox" id="chkAll"></th>';
}
print '<th>'.$langs->trans('MahnungRechnung').'</th>';
print '<th>'.$langs->trans('MahnungKunde').'</th>';
print '<th>'.$langs->trans('MahnungKontakt').'</th>';
print '<th>'.$langs->trans('MahnungKundentyp').'</th>';
print '<th>'.$langs->trans('MahnungFaelligkeitAlt').'</th>';
print '<th class="right">'.$langs->trans('MahnungTageVerzug').'</th>';
print '<th class="right">'.$langs->trans('MahnungBetragOffen').'</th>';
print '<th>'.$langs->trans('MahnungLetzteMahnung').'</th>';
print '<th>'.$langs->trans('MahnungVorgeschlageneStufe').'</th>';
print '</tr>';
$summeOffen = 0.0;
foreach ($rows as $r) {
print '<tr class="oddeven">';
if ($canWrite) {
print '<td><input type="checkbox" name="facture_ids[]" value="'.((int) $r['facture_id']).'" data-stufe="'.((int) $r['vorgeschlagene_stufe']).'"></td>';
}
print '<td><a href="'.DOL_URL_ROOT.'/compta/facture/card.php?id='.((int) $r['facture_id']).'">'.dol_escape_htmltag($r['facture_ref']).'</a></td>';
print '<td><a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.((int) $r['soc_id']).'">'.dol_escape_htmltag($r['soc_nom']).'</a></td>';
print '<td class="nowrap">'.renderKontaktIcons($r['soc_phone'] ?? '', $r['soc_email'] ?? '').'</td>';
print '<td>'.dol_escape_htmltag($r['kundentyp']).'</td>';
print '<td>'.dol_print_date($r['facture_date_lim_reglement'], 'day').'</td>';
print '<td class="right">'.((int) $r['tage_verzug']).'</td>';
print '<td class="right">'.price($r['betrag_offen']).'</td>';
print '<td>'.($r['letzte_mahnung_stufe'] ? $langs->trans('MahnungStufeAmDatum', ((int) $r['letzte_mahnung_stufe']), dol_print_date($r['letzte_mahnung_datum'], 'day')) : '—').'</td>';
print '<td><strong>'.((int) $r['vorgeschlagene_stufe']).'</strong> — '.dol_escape_htmltag($r['vorgeschlagene_stufe_label']).'</td>';
print '</tr>';
$summeOffen += (float) $r['betrag_offen'];
}
print '<tr class="liste_total"><td colspan="'.($canWrite ? 7 : 6).'" class="right">'.$langs->trans('Total').'</td>';
print '<td class="right">'.price($summeOffen).'</td><td colspan="2"></td></tr>';
print '</table>';
if ($canWrite) {
print '<br><div class="center">';
print '<button type="submit" class="button" name="action" value="bulk_create">'.$langs->trans('MahnungErstellen').'</button> ';
print '<button type="submit" class="button" name="action" value="bulk_sammelbrief" formaction="'.DOL_URL_ROOT.'/custom/mahnung/ajax/sammelbrief.php">'.$langs->trans('MahnungSammelbrief').'</button>';
print '</div>';
print '<script>document.getElementById("chkAll")?.addEventListener("change", function(e){';
print 'document.querySelectorAll("input[name=\'facture_ids[]\']").forEach(c => c.checked = e.target.checked);';
print '});</script>';
}
print '</form>';
renderUebersprungeneTabelle($skipped);
}
/**
* Diagnose-Tabelle: überfällige Rechnungen, die aktuell nicht vorgeschlagen werden,
* inklusive Grund (Wartefrist, Stufen ausgeschöpft, ...).
*
* @param array $skipped
* @return void
*/
function renderUebersprungeneTabelle($skipped)
{
global $langs;
if (empty($skipped)) {
return;
}
print '<br><h3>'.$langs->trans('MahnungUebersprungen').' ('.count($skipped).')</h3>';
print '<div class="opacitymedium">'.$langs->trans('MahnungUebersprungenHint').'</div>';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('MahnungRechnung').'</th>';
print '<th>'.$langs->trans('MahnungKunde').'</th>';
print '<th>'.$langs->trans('MahnungKontakt').'</th>';
print '<th>'.$langs->trans('MahnungFaelligkeitAlt').'</th>';
print '<th class="right">'.$langs->trans('MahnungTageVerzug').'</th>';
print '<th class="right">'.$langs->trans('MahnungBetragOffen').'</th>';
print '<th>'.$langs->trans('MahnungLetzteMahnung').'</th>';
print '<th>'.$langs->trans('MahnungSkipGrund').'</th>';
print '</tr>';
foreach ($skipped as $r) {
print '<tr class="oddeven">';
print '<td><a href="'.DOL_URL_ROOT.'/compta/facture/card.php?id='.((int) $r['facture_id']).'">'.dol_escape_htmltag($r['facture_ref']).'</a></td>';
print '<td><a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.((int) $r['soc_id']).'">'.dol_escape_htmltag($r['soc_nom']).'</a></td>';
print '<td class="nowrap">'.renderKontaktIcons($r['soc_phone'] ?? '', $r['soc_email'] ?? '').'</td>';
print '<td>'.dol_print_date($r['facture_date_lim_reglement'], 'day').'</td>';
print '<td class="right">'.((int) $r['tage_verzug']).'</td>';
print '<td class="right">'.price($r['betrag_offen']).'</td>';
print '<td>'.($r['letzte_mahnung_stufe'] ? $langs->trans('MahnungStufeAmDatum', ((int) $r['letzte_mahnung_stufe']), dol_print_date($r['letzte_mahnung_datum'], 'day')) : '—').'</td>';
print '<td><span class="opacitymedium">'.dol_escape_htmltag((string) $r['skip_reason']).'</span></td>';
print '</tr>';
}
print '</table>';
}
/**
* Rendert Kontakt-Icons (Telefon + Mail) mit Direktlinks.
*
* @param string $phone
* @param string $email
* @return string HTML
*/
function renderKontaktIcons($phone, $email)
{
$out = '';
$phone = trim((string) $phone);
$email = trim((string) $email);
if ($phone !== '') {
$telHref = preg_replace('/[^0-9+]/', '', $phone);
$out .= '<a href="tel:'.dol_escape_htmltag($telHref).'" title="'.dol_escape_htmltag($phone).'" class="marginrightonly">'
.img_picto('', 'fa-phone').'</a>';
}
if ($email !== '') {
$out .= '<a href="mailto:'.dol_escape_htmltag($email).'" title="'.dol_escape_htmltag($email).'">'
.img_picto('', 'fa-envelope').'</a>';
}
if ($out === '') {
$out = '<span class="opacitymedium">—</span>';
}
return $out;
}
/**
* Rendert das Archiv aller bestehenden Mahnvorgänge.
*
* @param DoliDB $db
* @param array $filter
* @return void
*/
function renderArchiv($db, $filter)
{
global $langs;
$mahnungObj = new Mahnung($db);
$archivFilter = array();
if (isset($filter['stufe'])) {
$archivFilter['stufe'] = $filter['stufe'];
}
$mahnungen = $mahnungObj->fetchAll('date_mahnung', 'DESC', 200, 0, $archivFilter);
if (empty($mahnungen)) {
print '<div class="info">'.$langs->trans('MahnungKeineVorgaenge').'</div>';
return;
}
// Refs für alle in der Liste vorkommenden Rechnungen+Kunden in zwei Queries holen
$socIds = array();
$factIds = array();
foreach ($mahnungen as $m) {
$socIds[(int) $m->fk_soc] = true;
$factIds[(int) $m->fk_facture] = true;
}
$socMap = array();
if (!empty($socIds)) {
$resql = $db->query("SELECT rowid, nom FROM ".MAIN_DB_PREFIX."societe WHERE rowid IN (".implode(',', array_keys($socIds)).")");
while ($resql && ($r = $db->fetch_object($resql))) {
$socMap[(int) $r->rowid] = $r->nom;
}
}
$factMap = array();
if (!empty($factIds)) {
$resql = $db->query("SELECT rowid, ref FROM ".MAIN_DB_PREFIX."facture WHERE rowid IN (".implode(',', array_keys($factIds)).")");
while ($resql && ($r = $db->fetch_object($resql))) {
$factMap[(int) $r->rowid] = $r->ref;
}
}
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('MahnungRef').'</th>';
print '<th>'.$langs->trans('MahnungRechnung').'</th>';
print '<th>'.$langs->trans('MahnungKunde').'</th>';
print '<th>'.$langs->trans('MahnungStufe').'</th>';
print '<th>'.$langs->trans('MahnungDatum').'</th>';
print '<th class="right">'.$langs->trans('MahnungBetragOffen').'</th>';
print '<th class="right">'.$langs->trans('MahnungGebuehr').'</th>';
print '<th class="right">'.$langs->trans('MahnungVerzugszinsen').'</th>';
print '<th class="right">'.$langs->trans('MahnungSumme').'</th>';
print '<th>'.$langs->trans('Status').'</th>';
print '</tr>';
foreach ($mahnungen as $m) {
$factureRef = $factMap[(int) $m->fk_facture] ?? ('#'.(int) $m->fk_facture);
$socName = $socMap[(int) $m->fk_soc] ?? ('#'.(int) $m->fk_soc);
print '<tr class="oddeven">';
print '<td><a href="'.DOL_URL_ROOT.'/custom/mahnung/card.php?id='.((int) $m->id).'">'.dol_escape_htmltag($m->ref).'</a></td>';
print '<td><a href="'.DOL_URL_ROOT.'/compta/facture/card.php?id='.((int) $m->fk_facture).'">'.dol_escape_htmltag($factureRef).'</a></td>';
print '<td><a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.((int) $m->fk_soc).'">'.dol_escape_htmltag($socName).'</a></td>';
print '<td>'.((int) $m->stufe).'</td>';
print '<td>'.dol_print_date($m->date_mahnung, 'day').'</td>';
print '<td class="right">'.price($m->betrag_offen).'</td>';
print '<td class="right">'.price((float) $m->mahngebuehr + (float) $m->pauschale_b2b).'</td>';
print '<td class="right">'.price($m->verzugszinsen).'</td>';
print '<td class="right"><strong>'.price($m->summe_mahnung).'</strong></td>';
print '<td>'.dol_escape_htmltag($m->getStatusLabel()).'</td>';
print '</tr>';
}
print '</table>';
}