Archiv zeigt Kundennamen, Vorschlagsliste mit Skip-Diagnose [deploy]
All checks were successful
Deploy mahnung / deploy (push) Successful in 13s
All checks were successful
Deploy mahnung / deploy (push) Successful in 13s
- list.php Archiv: Rechnungs-Ref + Kunden-Name per Bulk-Query (statt rowid). Mahnung-Ref klickt zur Detailansicht (card.php). - MahnungVorschlag: neue buildAlleVorschlaege()/getUebersprungeneRechnungen() liefern auch ueberfaellige Rechnungen ohne aktuellen Vorschlag inkl. skip_reason (Wartefrist laeuft, Stufen ausgeschoepft, Frist Stufe 1 nicht erreicht, ...). - list.php Vorschlagsliste: zweite Tabelle "Aktuell uebersprungen" mit Grund. Erklaert warum ueberfaellige Rechnungen nicht in der Vorschlagsliste auftauchen. - Lang-Keys MahnungUebersprungen/Hint/SkipGrund (DE+EN). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d1db85322b
commit
d889fb25a5
4 changed files with 186 additions and 19 deletions
|
|
@ -108,14 +108,89 @@ class MahnungVorschlag
|
|||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert alle ueberfaelligen Rechnungen, fuer die aktuell KEIN Vorschlag passt,
|
||||
* inkl. Begruendung (skip_reason). Diagnose-Hilfe fuer das UI.
|
||||
*
|
||||
* @param array $filter (siehe getVorschlaege)
|
||||
* @return array
|
||||
*/
|
||||
public function getUebersprungeneRechnungen(array $filter = array())
|
||||
{
|
||||
$rows = $this->buildAlleVorschlaege($filter);
|
||||
$skipped = array();
|
||||
foreach ($rows as $r) {
|
||||
if ($r['vorgeschlagene_stufe'] === null) {
|
||||
$skipped[] = $r;
|
||||
}
|
||||
}
|
||||
return $skipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert sowohl vorgeschlagene als auch uebersprungene Rechnungen in einem Durchlauf.
|
||||
* Result-Schluessel je Eintrag wie bei getVorschlaege(), zusaetzlich:
|
||||
* skip_reason (string|null)
|
||||
*
|
||||
* @param array $filter
|
||||
* @return array
|
||||
*/
|
||||
public function buildAlleVorschlaege(array $filter = array())
|
||||
{
|
||||
$this->loadStufen();
|
||||
if (empty($this->stufen)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$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.statut,";
|
||||
$sql .= " s.nom AS soc_nom, s.tva_intra";
|
||||
$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);
|
||||
$sql .= " AND f.statut = 1";
|
||||
$sql .= " AND f.paye = 0";
|
||||
$sql .= " AND f.type IN (0, 2, 3)";
|
||||
$sql .= " AND f.date_lim_reglement IS NOT NULL";
|
||||
$sql .= " AND f.date_lim_reglement < '".$this->db->idate($today)."'";
|
||||
|
||||
if (!empty($filter['soc_id'])) {
|
||||
$sql .= " AND f.fk_soc = ".((int) $filter['soc_id']);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY f.date_lim_reglement ASC";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
dol_syslog('MahnungVorschlag::buildAlleVorschlaege SQL-Fehler: '.$this->db->lasterror(), LOG_ERR);
|
||||
return array();
|
||||
}
|
||||
|
||||
$result = array();
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$row = $this->buildVorschlag($obj, $today, true);
|
||||
if ($row === null) {
|
||||
continue;
|
||||
}
|
||||
if (isset($filter['min_tage_verzug']) && $row['tage_verzug'] < (int) $filter['min_tage_verzug']) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $row;
|
||||
}
|
||||
$this->db->free($resql);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet fuer eine einzelne Rechnung, ob/wozu eine Mahnung vorgeschlagen wird.
|
||||
*
|
||||
* @param object $factureObj DB-Reihe aus facture+societe
|
||||
* @param int $today Unix-Zeit
|
||||
* @param bool $includeSkipped true = liefert auch uebersprungene mit skip_reason
|
||||
* @return array|null
|
||||
*/
|
||||
private function buildVorschlag($factureObj, $today)
|
||||
private function buildVorschlag($factureObj, $today, $includeSkipped = false)
|
||||
{
|
||||
$dateLim = $this->db->jdate($factureObj->date_lim_reglement);
|
||||
if (empty($dateLim)) {
|
||||
|
|
@ -133,33 +208,43 @@ class MahnungVorschlag
|
|||
|
||||
// Naechste Stufe ermitteln
|
||||
$proposedStufe = null;
|
||||
$skipReason = null;
|
||||
if ($lastMahnung === null) {
|
||||
// Noch nichts gemahnt -> Stufe 1, sobald frist_tage erreicht
|
||||
if (isset($this->stufen[1]) && $tageVerzug >= (int) $this->stufen[1]->frist_tage) {
|
||||
$frist1 = isset($this->stufen[1]) ? (int) $this->stufen[1]->frist_tage : 0;
|
||||
if (!isset($this->stufen[1])) {
|
||||
$skipReason = 'Stufe 1 nicht konfiguriert';
|
||||
} elseif ($tageVerzug >= $frist1) {
|
||||
$proposedStufe = 1;
|
||||
} else {
|
||||
$skipReason = 'Frist Stufe 1 ('.$frist1.' Tage) noch nicht erreicht (Verzug '.$tageVerzug.' Tage)';
|
||||
}
|
||||
} else {
|
||||
// Bereits gemahnt -> naechste Stufe wenn Wartefrist seit letzter Mahnung abgelaufen
|
||||
$lastStufe = (int) $lastMahnung->stufe;
|
||||
$nextStufe = $lastStufe + 1;
|
||||
if ($lastStufe >= 3 || !isset($this->stufen[$nextStufe])) {
|
||||
return null; // alle Stufen ausgeschoepft
|
||||
$skipReason = 'Alle Mahnstufen ausgeschoepft (zuletzt Stufe '.$lastStufe.')';
|
||||
} else {
|
||||
$wartefrist = isset($this->stufen[$lastStufe]) ? (int) $this->stufen[$lastStufe]->neue_frist_tage : 7;
|
||||
$tageSeitMahnung = (int) floor(($today - $lastMahnung->date_mahnung) / 86400);
|
||||
if ($tageSeitMahnung >= $wartefrist) {
|
||||
$proposedStufe = $nextStufe;
|
||||
} else {
|
||||
$skipReason = 'Wartefrist nach Stufe '.$lastStufe.' laeuft noch ('.$tageSeitMahnung.'/'.$wartefrist.' Tage)';
|
||||
}
|
||||
}
|
||||
// Wartefrist: neue_frist_tage der zuletzt gemahnten Stufe
|
||||
$wartefrist = isset($this->stufen[$lastStufe]) ? (int) $this->stufen[$lastStufe]->neue_frist_tage : 7;
|
||||
$tageSeitMahnung = (int) floor(($today - $lastMahnung->date_mahnung) / 86400);
|
||||
if ($tageSeitMahnung >= $wartefrist) {
|
||||
$proposedStufe = $nextStufe;
|
||||
}
|
||||
}
|
||||
|
||||
if ($proposedStufe === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Offenen Betrag berechnen (total_ttc - Summe aller Zahlungen)
|
||||
$betragOffen = $this->getBetragOffen((int) $factureObj->facture_id, (float) $factureObj->total_ttc);
|
||||
if ($betragOffen <= 0) {
|
||||
if (!$includeSkipped) {
|
||||
return null;
|
||||
}
|
||||
$proposedStufe = null;
|
||||
$skipReason = 'Offener Betrag <= 0 (vermutl. komplett bezahlt, paye-Flag noch nicht gesetzt)';
|
||||
}
|
||||
|
||||
if ($proposedStufe === null && !$includeSkipped) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +263,8 @@ class MahnungVorschlag
|
|||
'letzte_mahnung_stufe' => $lastMahnung ? (int) $lastMahnung->stufe : null,
|
||||
'letzte_mahnung_datum' => $lastMahnung ? $lastMahnung->date_mahnung : null,
|
||||
'vorgeschlagene_stufe' => $proposedStufe,
|
||||
'vorgeschlagene_stufe_label' => $this->stufen[$proposedStufe]->label,
|
||||
'vorgeschlagene_stufe_label' => $proposedStufe !== null ? $this->stufen[$proposedStufe]->label : null,
|
||||
'skip_reason' => $skipReason,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ MahnungErstellen = Mahnung erstellen
|
|||
MahnungSammelbrief = Sammelbrief erzeugen
|
||||
MahnungStornieren = Stornieren
|
||||
MahnungKeineUeberfaelligen = Keine ueberfaelligen Rechnungen vorhanden.
|
||||
MahnungUebersprungen = Aktuell uebersprungene Rechnungen
|
||||
MahnungUebersprungenHint = Diese Rechnungen sind ueberfaellig, werden aber aktuell nicht vorgeschlagen (Wartefrist laeuft noch oder alle Mahnstufen ausgeschoepft).
|
||||
MahnungSkipGrund = Grund
|
||||
|
||||
#
|
||||
# Setup-Seite
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ MahnungErstellen = Create dunning
|
|||
MahnungSammelbrief = Generate bulk letter
|
||||
MahnungStornieren = Cancel
|
||||
MahnungKeineUeberfaelligen = No overdue invoices found.
|
||||
MahnungUebersprungen = Currently skipped invoices
|
||||
MahnungUebersprungenHint = These invoices are overdue but currently not proposed (waiting period running or all dunning stages exhausted).
|
||||
MahnungSkipGrund = Reason
|
||||
|
||||
#
|
||||
# Setup page
|
||||
|
|
|
|||
81
list.php
81
list.php
|
|
@ -119,9 +119,16 @@ function renderVorschlagsliste($db, $filter)
|
|||
|
||||
$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;
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +182,49 @@ function renderVorschlagsliste($db, $filter)
|
|||
print '});</script>';
|
||||
}
|
||||
print '</form>';
|
||||
|
||||
renderUebersprungeneTabelle($skipped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnose-Tabelle: ueberfaellige Rechnungen, die aktuell nicht vorgeschlagen werden,
|
||||
* inklusive Grund (Wartefrist, Stufen ausgeschoepft, ...).
|
||||
*
|
||||
* @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('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>'.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'] ? 'Stufe '.((int) $r['letzte_mahnung_stufe']).' am '.dol_print_date($r['letzte_mahnung_datum'], 'day') : '—').'</td>';
|
||||
print '<td class="opacitymedium">'.dol_escape_htmltag((string) $r['skip_reason']).'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
print '</table>';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -200,6 +250,28 @@ function renderArchiv($db, $filter)
|
|||
return;
|
||||
}
|
||||
|
||||
// Refs fuer 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>';
|
||||
|
|
@ -215,10 +287,13 @@ function renderArchiv($db, $filter)
|
|||
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>'.dol_escape_htmltag($m->ref).'</td>';
|
||||
print '<td><a href="'.DOL_URL_ROOT.'/compta/facture/card.php?id='.((int) $m->fk_facture).'">#'.((int) $m->fk_facture).'</a></td>';
|
||||
print '<td><a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.((int) $m->fk_soc).'">#'.((int) $m->fk_soc).'</a></td>';
|
||||
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>';
|
||||
|
|
|
|||
Loading…
Reference in a new issue