From d889fb25a57eba3212be818d00196bf15f38e232 Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Thu, 7 May 2026 12:16:20 +0200 Subject: [PATCH] Archiv zeigt Kundennamen, Vorschlagsliste mit Skip-Diagnose [deploy] - 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) --- class/mahnungvorschlag.class.php | 118 ++++++++++++++++++++++++++----- langs/de_DE/mahnung.lang | 3 + langs/en_US/mahnung.lang | 3 + list.php | 81 ++++++++++++++++++++- 4 files changed, 186 insertions(+), 19 deletions(-) diff --git a/class/mahnungvorschlag.class.php b/class/mahnungvorschlag.class.php index 826a15d..2b81ae0 100644 --- a/class/mahnungvorschlag.class.php +++ b/class/mahnungvorschlag.class.php @@ -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, ); } diff --git a/langs/de_DE/mahnung.lang b/langs/de_DE/mahnung.lang index 9d33ff2..be23383 100644 --- a/langs/de_DE/mahnung.lang +++ b/langs/de_DE/mahnung.lang @@ -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 diff --git a/langs/en_US/mahnung.lang b/langs/en_US/mahnung.lang index bf4e19c..5acdbfb 100644 --- a/langs/en_US/mahnung.lang +++ b/langs/en_US/mahnung.lang @@ -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 diff --git a/list.php b/list.php index 507c6dc..70bc613 100644 --- a/list.php +++ b/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 '
'.$langs->trans('MahnungKeineUeberfaelligen').'
'; + return; + } if (empty($rows)) { print '
'.$langs->trans('MahnungKeineUeberfaelligen').'
'; + renderUebersprungeneTabelle($skipped); return; } @@ -175,6 +182,49 @@ function renderVorschlagsliste($db, $filter) print '});'; } print ''; + + 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 '

'.$langs->trans('MahnungUebersprungen').' ('.count($skipped).')

'; + print '
'.$langs->trans('MahnungUebersprungenHint').'
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + foreach ($skipped as $r) { + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } + print '
'.$langs->trans('MahnungRechnung').''.$langs->trans('MahnungKunde').''.$langs->trans('MahnungFaelligkeitAlt').''.$langs->trans('MahnungTageVerzug').''.$langs->trans('MahnungBetragOffen').''.$langs->trans('MahnungLetzteMahnung').''.$langs->trans('MahnungSkipGrund').'
'.dol_escape_htmltag($r['facture_ref']).''.dol_escape_htmltag($r['soc_nom']).''.dol_print_date($r['facture_date_lim_reglement'], 'day').''.((int) $r['tage_verzug']).''.price($r['betrag_offen']).''.($r['letzte_mahnung_stufe'] ? 'Stufe '.((int) $r['letzte_mahnung_stufe']).' am '.dol_print_date($r['letzte_mahnung_datum'], 'day') : '—').''.dol_escape_htmltag((string) $r['skip_reason']).'
'; } /** @@ -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 ''; print ''; print ''; @@ -215,10 +287,13 @@ function renderArchiv($db, $filter) print ''; 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 ''; - print ''; - print ''; - print ''; + print ''; + print ''; + print ''; print ''; print ''; print '';
'.$langs->trans('MahnungRef').'
'.dol_escape_htmltag($m->ref).'#'.((int) $m->fk_facture).'#'.((int) $m->fk_soc).''.dol_escape_htmltag($m->ref).''.dol_escape_htmltag($factureRef).''.dol_escape_htmltag($socName).''.((int) $m->stufe).''.dol_print_date($m->date_mahnung, 'day').''.price($m->betrag_offen).'