All checks were successful
Deploy mahnung / deploy (push) Successful in 13s
Vorher: Flex-Spacer-Trick (flex-basis:100%) sollte den Button in zweite Zeile zwingen — funktioniert aber nur wenn .tabsAction ein Flexbox-Container mit flex-wrap ist. Im AwlDark-Theme ist das nicht der Fall → Button landete einfach am Anfang der Aktionsleiste. Jetzt: kurzes JS-Snippet verschiebt #btn-mahnung-create per insertBefore vor den ersten .butActionRefused (= disabled "Löschen" / "Auf anderen Kunden übertragen" — typisch sichtbar bei freigegebener Rechnung). Fallback: appendTo ans Ende der .tabsAction. So landet "Mahnung erstellen" optisch direkt bei den Verwaltungs-Buttons am rechten/unteren Ende, wie Eddy es wollte. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
308 lines
10 KiB
PHP
308 lines
10 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* GPL v3 (siehe COPYING).
|
|
*/
|
|
|
|
/**
|
|
* \file htdocs/custom/mahnung/class/actions_mahnung.class.php
|
|
* \ingroup mahnung
|
|
* \brief Hook-Klasse: Tab "Mahnungen" + Button "Mahnung erstellen" auf Rechnungs-Karte.
|
|
*
|
|
* Wird in modMahnung registriert via module_parts['hooks'] = ['data' => ['invoicecard']].
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php';
|
|
|
|
class ActionsMahnung
|
|
{
|
|
/** @var array */
|
|
public $errors = array();
|
|
|
|
/** @var string */
|
|
public $resprints = '';
|
|
|
|
/**
|
|
* Hook addMoreActionsButtons: Button "Mahnung erstellen" im Header der Rechnungs-Karte.
|
|
*
|
|
* @param array $parameters
|
|
* @param CommonObject $object Facture
|
|
* @param string $action
|
|
* @param HookManager $hookmanager
|
|
* @return int 0 = weiter, 1 = überschreiben
|
|
*/
|
|
public function addMoreActionsButtons($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $user, $langs;
|
|
|
|
$contexts = explode(':', $parameters['context'] ?? '');
|
|
if (!in_array('invoicecard', $contexts, true)) {
|
|
return 0;
|
|
}
|
|
|
|
if (empty($object->id) || empty($object->socid)) {
|
|
return 0;
|
|
}
|
|
|
|
// Nur für normale Kundenrechnungen
|
|
if (!isset($object->type) || (int) $object->type !== 0) {
|
|
return 0;
|
|
}
|
|
|
|
$paye = (int) ($object->paye ?? 0);
|
|
$statut = (int) ($object->statut ?? ($object->status ?? 0));
|
|
|
|
// Button nur bei FREIGEGEBENER (statut >= 1) und NOCH OFFENER (paye === 0) Rechnung.
|
|
// Entwurf, bezahlt, storniert, abandoned → gar kein Button.
|
|
if ($statut < 1 || $paye !== 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (!$user->hasRight('mahnung', 'write')) {
|
|
return 0;
|
|
}
|
|
|
|
$langs->load('mahnung@mahnung');
|
|
|
|
$dateLim = !empty($object->date_lim_reglement) ? (int) $object->date_lim_reglement : 0;
|
|
$ueberfaellig = ($dateLim > 0 && $dateLim < dol_now());
|
|
|
|
$label = $langs->trans('MahnungErstellen');
|
|
|
|
if ($ueberfaellig) {
|
|
$url = DOL_URL_ROOT.'/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=vorschlag&search_socid='.((int) $object->socid);
|
|
$btn = dolGetButtonAction($label, '', 'default', $url, 'btn-mahnung-create', 1);
|
|
} else {
|
|
$attr = array('title' => $langs->trans('MahnungKeineUeberfaelligen'));
|
|
$btn = dolGetButtonAction($label, '', 'default', '#', 'btn-mahnung-create', 0, array('attr' => $attr));
|
|
}
|
|
|
|
// Der Hook läuft VOR den Standard-Buttons → der Mahnung-Button würde sonst ganz
|
|
// am Anfang der .tabsAction-Div stehen. Eddy will ihn aber bei den letzten
|
|
// "Verwaltungs"-Buttons (Löschen / Auf anderen Kunden übertragen — typisch
|
|
// butActionRefused weil disabled bei freigegebener Rechnung).
|
|
// JS verschiebt den Button per insertBefore VOR den ersten .butActionRefused —
|
|
// fällt zurück auf appendTo wenn keiner existiert.
|
|
$js = "<script>jQuery(function(\$){";
|
|
$js .= "var \$btn=\$('#btn-mahnung-create');";
|
|
$js .= "if(!\$btn.length)return;";
|
|
$js .= "var \$ref=\$('.tabsAction .butActionRefused').first();";
|
|
$js .= "if(\$ref.length){\$btn.insertBefore(\$ref);}";
|
|
$js .= "else{\$btn.appendTo('.tabsAction');}";
|
|
$js .= "});</script>";
|
|
|
|
// WICHTIG: addMoreActionsButtons braucht direkten print, nicht $this->resprints.
|
|
// card.php wertet zwar $reshook aus, druckt aber $hookmanager->resPrint an dieser
|
|
// Stelle NICHT — anders als bei formObjectOptions.
|
|
print $btn.$js;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Hook completeTabsHead: Tab "Mahnungen (n)" sowohl auf Rechnungs- als
|
|
* auch auf Kundenkarte einblenden.
|
|
*
|
|
* @param array $parameters
|
|
* @param CommonObject $object Facture oder Societe
|
|
* @param string $action
|
|
* @param HookManager $hookmanager
|
|
* @return int 0 = weiter
|
|
*/
|
|
public function completeTabsHead($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $langs, $db;
|
|
|
|
$contexts = explode(':', $parameters['context'] ?? '');
|
|
$onInvoice = in_array('invoicecard', $contexts, true);
|
|
$onThirdparty = in_array('thirdpartycard', $contexts, true);
|
|
if (!$onInvoice && !$onThirdparty) {
|
|
return 0;
|
|
}
|
|
if (empty($object->id)) {
|
|
return 0;
|
|
}
|
|
if (!isset($parameters['head']) || !is_array($parameters['head'])) {
|
|
return 0;
|
|
}
|
|
|
|
// complete_head_from_modules() wird pro Karte mehrfach aufgerufen
|
|
// (mode='add' für 'core' UND 'external', plus mode='remove').
|
|
// Genau einmal feuern: nur add/external.
|
|
if (($parameters['mode'] ?? '') !== 'add') {
|
|
return 0;
|
|
}
|
|
if (($parameters['filterorigmodule'] ?? '') !== 'external') {
|
|
return 0;
|
|
}
|
|
|
|
$langs->load('mahnung@mahnung');
|
|
|
|
if ($onInvoice) {
|
|
$count = $this->countMahnungen($db, 'fk_facture', (int) $object->id);
|
|
$tabUrl = DOL_URL_ROOT.'/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=archiv&fk_facture='.((int) $object->id);
|
|
} else {
|
|
$count = $this->countMahnungen($db, 'fk_soc', (int) $object->id);
|
|
$tabUrl = DOL_URL_ROOT.'/custom/mahnung/list.php?mainmenu=billing&leftmenu=mahnung&mode=archiv&search_socid='.((int) $object->id);
|
|
}
|
|
|
|
$head = &$parameters['head'];
|
|
$pos = count($head);
|
|
$head[$pos][0] = $tabUrl;
|
|
$head[$pos][1] = $langs->trans('MahnungMenu').($count > 0 ? ' <span class="badge">'.$count.'</span>' : '');
|
|
$head[$pos][2] = 'mahnung';
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param DoliDB $db
|
|
* @param string $col 'fk_facture' | 'fk_soc'
|
|
* @param int $id
|
|
* @return int
|
|
*/
|
|
private function countMahnungen($db, $col, $id)
|
|
{
|
|
if (!in_array($col, array('fk_facture', 'fk_soc'), true)) {
|
|
return 0;
|
|
}
|
|
$sql = "SELECT COUNT(*) AS n FROM ".MAIN_DB_PREFIX."mahnung_mahnung";
|
|
$sql .= " WHERE ".$col." = ".((int) $id);
|
|
$sql .= " AND status NOT IN (".Mahnung::STATUS_STORNIERT.")";
|
|
$resql = $db->query($sql);
|
|
if (!$resql) {
|
|
return 0;
|
|
}
|
|
$obj = $db->fetch_object($resql);
|
|
$count = (int) $obj->n;
|
|
$db->free($resql);
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Hook tabContentViewThirdparty: rendert eine prominente Bonitäts-Warnbox auf
|
|
* der Kundenkarte, wenn für diesen Kunden uneinbringliche Forderungen existieren
|
|
* (fk_statut=3 abandoned + close_code='badcustomer').
|
|
*
|
|
* @param array $parameters
|
|
* @param CommonObject $object Societe
|
|
* @param string $action
|
|
* @param HookManager $hookmanager
|
|
* @return int 0
|
|
*/
|
|
public function tabContentViewThirdparty($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $langs, $db;
|
|
|
|
if (empty($object->id)) {
|
|
return 0;
|
|
}
|
|
$langs->load('mahnung@mahnung');
|
|
|
|
$info = $this->getBonitaetsInfo($db, (int) $object->id);
|
|
if (empty($info) || $info['anzahl'] <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Rote Warnbox direkt unter dem Banner — vor dem Standard-Content
|
|
print '<div class="warning" style="margin:8px 0; padding:12px; border-left:5px solid #c33; background:#fee2; border-radius:4px;">';
|
|
print '<div style="font-size:1.1em; font-weight:bold; color:#c33; margin-bottom:6px;">';
|
|
print img_picto('', 'warning', 'class="pictofixedwidth"');
|
|
print dol_escape_htmltag($langs->trans('MahnungBonitaetWarnung')).'</div>';
|
|
print '<table class="noborder" style="margin:0;"><tr>';
|
|
print '<td><strong>'.((int) $info['anzahl']).'</strong> '.dol_escape_htmltag($langs->trans($info['anzahl'] === 1 ? 'MahnungBonitaetRechnungSing' : 'MahnungBonitaetRechnungPlur')).'</td>';
|
|
print '<td style="padding-left:30px;"><strong>'.price($info['summe']).'</strong> '.dol_escape_htmltag($langs->trans('MahnungBonitaetGesamtAusfall')).'</td>';
|
|
if (!empty($info['letztes_datum'])) {
|
|
print '<td style="padding-left:30px;" class="opacitymedium">'.dol_escape_htmltag($langs->trans('MahnungBonitaetLetzteAm')).' '.dol_print_date($info['letztes_datum'], 'day').'</td>';
|
|
}
|
|
print '</tr></table>';
|
|
// Link zur Detail-Liste (gefilterter Mahnung-Archiv-View)
|
|
$listUrl = DOL_URL_ROOT.'/compta/facture/list.php?socid='.((int) $object->id).'&search_status=3';
|
|
print '<div style="margin-top:6px;"><a href="'.$listUrl.'">'.dol_escape_htmltag($langs->trans('MahnungBonitaetAusfaelleAnzeigen')).' →</a></div>';
|
|
print '</div>';
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Hook addMoreActionsButtons: Warning-Banner bei der Aktions-Leiste falls
|
|
* der Kunde Forderungsausfälle hat. Wird sowohl bei Auftrag (ordercard) als
|
|
* auch bei Rechnung (invoicecard) gezeigt.
|
|
*
|
|
* Ergänzt addMoreActionsButtons (Mahnung-erstellen-Button) — wird vor
|
|
* jenem Block ausgegeben.
|
|
*
|
|
* @param array $parameters
|
|
* @param CommonObject $object
|
|
* @param string $action
|
|
* @param HookManager $hookmanager
|
|
* @return int
|
|
*/
|
|
public function formObjectOptions($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $langs, $db;
|
|
|
|
$contexts = explode(':', $parameters['context'] ?? '');
|
|
if (!in_array('invoicecard', $contexts, true) && !in_array('ordercard', $contexts, true)) {
|
|
return 0;
|
|
}
|
|
if (empty($object->socid) && empty($object->fk_soc)) {
|
|
return 0;
|
|
}
|
|
$socid = (int) ($object->socid ?? $object->fk_soc);
|
|
if ($socid <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
$langs->load('mahnung@mahnung');
|
|
$info = $this->getBonitaetsInfo($db, $socid);
|
|
if (empty($info) || $info['anzahl'] <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Kompakte Warn-Zeile — unaufdringlich, aber sichtbar
|
|
print '<div class="warning" style="margin:8px 0; padding:8px 12px; border-left:4px solid #c33; background:#fef2f2; border-radius:3px;">';
|
|
print img_picto('', 'warning', 'class="pictofixedwidth"');
|
|
print '<strong>'.dol_escape_htmltag($langs->trans('MahnungBonitaetWarnungKurz')).':</strong> ';
|
|
print sprintf(
|
|
$langs->trans('MahnungBonitaetKundeHat'),
|
|
(int) $info['anzahl'],
|
|
price($info['summe'])
|
|
);
|
|
print '</div>';
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Liefert Anzahl, Summe und Datum der letzten als 'badcustomer' abandonierten
|
|
* Rechnungen für einen Kunden.
|
|
*
|
|
* @param DoliDB $db
|
|
* @param int $socid
|
|
* @return array{anzahl:int, summe:float, letztes_datum:int|null}|null
|
|
*/
|
|
private function getBonitaetsInfo($db, $socid)
|
|
{
|
|
if ($socid <= 0) {
|
|
return null;
|
|
}
|
|
$sql = "SELECT COUNT(*) AS nb, COALESCE(SUM(total_ttc), 0) AS summe, MAX(date_closing) AS letztes_datum";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."facture";
|
|
$sql .= " WHERE fk_soc = ".((int) $socid);
|
|
$sql .= " AND fk_statut = 3";
|
|
$sql .= " AND close_code = 'badcustomer'";
|
|
$sql .= " AND entity IN (".getEntity('facture').")";
|
|
|
|
$resql = $db->query($sql);
|
|
if (!$resql) {
|
|
return null;
|
|
}
|
|
$obj = $db->fetch_object($resql);
|
|
$db->free($resql);
|
|
return array(
|
|
'anzahl' => (int) $obj->nb,
|
|
'summe' => (float) $obj->summe,
|
|
'letztes_datum' => !empty($obj->letztes_datum) ? $db->jdate($obj->letztes_datum) : null,
|
|
);
|
|
}
|
|
}
|