Vorschlagsliste: Kunden-Select2 + Mindestbetrag + Kundentyp-Filter + Kontakt-Spalte [deploy]
All checks were successful
Deploy mahnung / deploy (push) Successful in 14s

Filter-Zeile:
- Kunden-Filter "rowid"-Input ersetzt durch $form->select_company()
  (Ajax-Suche falls COMPANY_USE_SEARCH_TO_SELECT gesetzt, sonst klassisches
  Dropdown). Direkt-Links ?search_socid=74 von der Kundenkarte bleiben
  weiterhin funktional (htmlname=search_socid, Backward-kompatibel).
- Neuer Filter "Mindestbetrag" (EUR, Komma zugelassen).
- Neuer Filter "Kundentyp" (alle / B2B / B2C).

Tabelle:
- Neue Spalte "Kontakt" mit Telefon- und Mail-Direktlink-Icons (tel: / mailto:).
- Spalte erscheint sowohl in der Vorschlags- als auch in der Uebersprungen-Tabelle.

MahnungVorschlag::getVorschlaege() + buildAlleVorschlaege():
- SELECT erweitert um s.phone + s.email; werden als soc_phone/soc_email
  pro Eintrag mitgegeben.
- Neue PHP-side Filter min_betrag und kundentyp.

Lang-Keys: MahnungKontakt (de_DE + en_US).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-05-11 11:49:31 +02:00
parent e4f2dd6fb2
commit 8e5e26c162
5 changed files with 103 additions and 6 deletions

View file

@ -2,6 +2,12 @@
## [Unreleased]
### Vorschlagsliste — UX
- Kunden-Filter: rowid-Input ersetzt durch Dolibarr-Standard `select_company()` (Ajax-Suche bzw. klassisches Dropdown, je nach Dolibarr-Konfiguration). Direkt-Links `?search_socid=74` bleiben funktional.
- Neuer Filter "Mindestbetrag" (in EUR, Komma erlaubt).
- Neuer Filter "Kundentyp" (B2B / B2C).
- Neue Spalte "Kontakt" mit Telefon- und Mail-Direktlink-Icons.
### Fixes
- Kundenkarte: Tab "Mahnwesen" erschien doppelt, weil `complete_head_from_modules()` pro Karte mehrfach (core + external + remove) feuert. Hook filtert jetzt auf `mode=add` + `filterorigmodule=external`.

View file

@ -52,7 +52,8 @@ class MahnungVorschlag
* vorgeschlagene_stufe (int 1..3),
* vorgeschlagene_stufe_label (string)
*
* @param array $filter Optional: 'soc_id', 'min_tage_verzug', 'max_tage_verzug', 'stufe'
* @param array $filter Optional: 'soc_id', 'min_tage_verzug', 'max_tage_verzug', 'stufe',
* 'min_betrag' (float), 'kundentyp' ('B2B'|'B2C')
* @return array
*/
public function getVorschlaege(array $filter = array())
@ -65,7 +66,7 @@ class MahnungVorschlag
$today = dol_now();
$sql = "SELECT f.rowid AS facture_id, f.ref AS facture_ref, f.date_lim_reglement,";
$sql .= " f.total_ttc, f.fk_soc, f.paye, f.fk_statut,";
$sql .= " s.nom AS soc_nom, s.tva_intra";
$sql .= " s.nom AS soc_nom, s.tva_intra, s.phone AS soc_phone, s.email AS soc_email";
$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = f.fk_soc";
$sql .= " WHERE f.entity = ".((int) $this->entity);
@ -102,6 +103,12 @@ class MahnungVorschlag
if (isset($filter['stufe']) && $filter['stufe'] !== '' && (int) $row['vorgeschlagene_stufe'] !== (int) $filter['stufe']) {
continue;
}
if (isset($filter['min_betrag']) && (float) $row['betrag_offen'] < (float) $filter['min_betrag']) {
continue;
}
if (!empty($filter['kundentyp']) && $row['kundentyp'] !== $filter['kundentyp']) {
continue;
}
$result[] = $row;
}
$this->db->free($resql);
@ -145,7 +152,7 @@ class MahnungVorschlag
$today = dol_now();
$sql = "SELECT f.rowid AS facture_id, f.ref AS facture_ref, f.date_lim_reglement,";
$sql .= " f.total_ttc, f.fk_soc, f.paye, f.fk_statut,";
$sql .= " s.nom AS soc_nom, s.tva_intra";
$sql .= " s.nom AS soc_nom, s.tva_intra, s.phone AS soc_phone, s.email AS soc_email";
$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = f.fk_soc";
$sql .= " WHERE f.entity = ".((int) $this->entity);
@ -176,6 +183,12 @@ class MahnungVorschlag
if (isset($filter['min_tage_verzug']) && $row['tage_verzug'] < (int) $filter['min_tage_verzug']) {
continue;
}
if (isset($filter['min_betrag']) && (float) $row['betrag_offen'] < (float) $filter['min_betrag']) {
continue;
}
if (!empty($filter['kundentyp']) && $row['kundentyp'] !== $filter['kundentyp']) {
continue;
}
$result[] = $row;
}
$this->db->free($resql);
@ -256,6 +269,8 @@ class MahnungVorschlag
'soc_id' => (int) $factureObj->fk_soc,
'soc_nom' => $factureObj->soc_nom,
'soc_tva_intra' => $factureObj->tva_intra,
'soc_phone' => $factureObj->soc_phone ?? '',
'soc_email' => $factureObj->soc_email ?? '',
'kundentyp' => $kundentyp,
'tage_verzug' => $tageVerzug,
'betrag_offen' => $betragOffen,

View file

@ -65,6 +65,7 @@ MahnungVersandNone = Kein Versand
MahnungRef = Mahnung-Nr.
MahnungRechnung = Rechnung
MahnungKunde = Kunde
MahnungKontakt = Kontakt
MahnungKundentyp = Typ
MahnungKundentypB2C = Privat (B2C)
MahnungKundentypB2B = Geschaeftlich (B2B)

View file

@ -65,6 +65,7 @@ MahnungVersandNone = No dispatch
MahnungRef = Dunning ref.
MahnungRechnung = Invoice
MahnungKunde = Customer
MahnungKontakt = Contact
MahnungKundentyp = Type
MahnungKundentypB2C = Private (B2C)
MahnungKundentypB2B = Business (B2B)

View file

@ -41,6 +41,7 @@ if (!$res) {
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'));
@ -49,6 +50,8 @@ if (!$user->hasRight('mahnung', 'read')) {
accessforbidden();
}
$form = new Form($db);
$mode = GETPOST('mode', 'aZ09');
if ($mode !== 'archiv') {
$mode = 'vorschlag';
@ -67,6 +70,17 @@ $filter_socid = GETPOST('search_socid', 'int');
if (!empty($filter_socid)) {
$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'));
@ -82,17 +96,46 @@ 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><td><select name="filter_stufe">';
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>';
print '<td><input type="number" name="search_socid" value="'.dol_escape_htmltag((string) $filter_socid).'" size="6" placeholder="rowid">';
// 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
print '<td><select name="filter_kundentyp">';
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-Kompatibilitaet
// zu Direkt-Links (?search_socid=74) von der Kundenkarte.
print '<td>';
print $form->select_company(
$filter_socid, // selected
'search_socid', // htmlname (Hidden-Input-Name)
'', // filter
'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>';
@ -143,6 +186,7 @@ function renderVorschlagsliste($db, $filter)
}
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>';
@ -159,6 +203,7 @@ function renderVorschlagsliste($db, $filter)
}
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>';
@ -168,7 +213,7 @@ function renderVorschlagsliste($db, $filter)
print '</tr>';
$summeOffen += (float) $r['betrag_offen'];
}
print '<tr class="liste_total"><td colspan="'.($canWrite ? 6 : 5).'" class="right">'.$langs->trans('Total').'</td>';
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>';
@ -207,6 +252,7 @@ function renderUebersprungeneTabelle($skipped)
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>';
@ -217,6 +263,7 @@ function renderUebersprungeneTabelle($skipped)
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>';
@ -227,6 +274,33 @@ function renderUebersprungeneTabelle($skipped)
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 Mahnvorgaenge.
*