* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 3. */ /** * \file htdocs/custom/mahnung/class/mahnungvorschlag.class.php * \ingroup mahnung * \brief Service: ueberfaellige Rechnungen einsammeln und je Rechnung * die naechste vorgeschlagene Mahnstufe ermitteln. * * Geteilte Logik zwischen Cron-Job (Ntfy-Push) und Vorschlagslisten-UI. */ require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php'; class MahnungVorschlag { /** @var DoliDB */ public $db; /** @var int */ public $entity; /** @var MahnungStufe[] indexed by stufe (1..3) */ private $stufen = array(); /** * @param DoliDB $db */ public function __construct($db) { global $conf; $this->db = $db; $this->entity = $conf->entity; } /** * Liefert pro ueberfaelliger Rechnung einen Vorschlag (oder ueberspringt sie, * wenn alle Stufen bereits durchlaufen sind oder die Wartefrist noch laeuft). * * Rueckgabe-Schluessel je Eintrag: * facture_id, facture_ref, facture_date_lim_reglement (Unix), facture_total_ttc, * soc_id, soc_nom, soc_tva_intra, * kundentyp ('B2C'|'B2B'), * tage_verzug, * betrag_offen, * letzte_mahnung_id (int|null), letzte_mahnung_stufe (int|null), letzte_mahnung_datum (Unix|null), * vorgeschlagene_stufe (int 1..3), * vorgeschlagene_stufe_label (string) * * @param array $filter Optional: 'soc_id', 'min_tage_verzug', 'max_tage_verzug', 'stufe' * @return array */ public function getVorschlaege(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)"; // Standard, Avoir, Acompte (keine Replacements) $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::getVorschlaege SQL-Fehler: '.$this->db->lasterror(), LOG_ERR); return array(); } $result = array(); while ($obj = $this->db->fetch_object($resql)) { $row = $this->buildVorschlag($obj, $today); if ($row === null) { continue; } if (isset($filter['min_tage_verzug']) && $row['tage_verzug'] < (int) $filter['min_tage_verzug']) { continue; } if (isset($filter['max_tage_verzug']) && $row['tage_verzug'] > (int) $filter['max_tage_verzug']) { continue; } if (isset($filter['stufe']) && $filter['stufe'] !== '' && (int) $row['vorgeschlagene_stufe'] !== (int) $filter['stufe']) { 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 * @return array|null */ private function buildVorschlag($factureObj, $today) { $dateLim = $this->db->jdate($factureObj->date_lim_reglement); if (empty($dateLim)) { return null; } $tageVerzug = (int) floor(($today - $dateLim) / 86400); if ($tageVerzug < 0) { $tageVerzug = 0; } $kundentyp = !empty($factureObj->tva_intra) ? Mahnung::KUNDENTYP_B2B : Mahnung::KUNDENTYP_B2C; // Letzte aktive Mahnung zur Rechnung holen $lastMahnung = (new Mahnung($this->db))->fetchLastByFacture((int) $factureObj->facture_id); // Naechste Stufe ermitteln $proposedStufe = 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) { $proposedStufe = 1; } } 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 } // 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) { return null; } return array( 'facture_id' => (int) $factureObj->facture_id, 'facture_ref' => $factureObj->facture_ref, 'facture_date_lim_reglement' => $dateLim, 'facture_total_ttc' => (float) $factureObj->total_ttc, 'soc_id' => (int) $factureObj->fk_soc, 'soc_nom' => $factureObj->soc_nom, 'soc_tva_intra' => $factureObj->tva_intra, 'kundentyp' => $kundentyp, 'tage_verzug' => $tageVerzug, 'betrag_offen' => $betragOffen, 'letzte_mahnung_id' => $lastMahnung ? (int) $lastMahnung->id : null, '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, ); } /** * Offener Betrag = total_ttc - SUM(paiement.amount). * * @param int $factureId * @param float $totalTtc * @return float */ private function getBetragOffen($factureId, $totalTtc) { $sql = "SELECT COALESCE(SUM(pf.amount), 0) AS gezahlt"; $sql .= " FROM ".MAIN_DB_PREFIX."paiement_facture as pf"; $sql .= " WHERE pf.fk_facture = ".((int) $factureId); $resql = $this->db->query($sql); if (!$resql) { return (float) $totalTtc; } $obj = $this->db->fetch_object($resql); $this->db->free($resql); return round(((float) $totalTtc) - ((float) $obj->gezahlt), 2); } /** * Stufen einmal in $this->stufen[1..3] cachen. */ private function loadStufen() { if (!empty($this->stufen)) { return; } $so = new MahnungStufe($this->db); foreach ($so->fetchAllActive() as $s) { $this->stufen[(int) $s->stufe] = $s; } } /** * Gibt die geladene MahnungStufe (1..3) zurueck oder null. * * @param int $stufe * @return MahnungStufe|null */ public function getStufe($stufe) { $this->loadStufen(); return $this->stufen[(int) $stufe] ?? null; } }