* * 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/mahnung.class.php * \ingroup mahnung * \brief CRUD-Klasse für Mahnvorgänge (llx_mahnung_mahnung). */ require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; /** * Klasse Mahnung — repräsentiert einen Mahnvorgang zu einer Kundenrechnung. */ class Mahnung extends CommonObject { const STATUS_ENTWURF = 0; const STATUS_ERSTELLT = 1; const STATUS_VERSENDET = 2; const STATUS_ERLEDIGT = 3; const STATUS_STORNIERT = 9; const VERSAND_PDF = 'pdf'; const VERSAND_MAIL = 'mail'; const VERSAND_DRUCK = 'druck'; const VERSAND_NONE = 'none'; const KUNDENTYP_B2C = 'B2C'; const KUNDENTYP_B2B = 'B2B'; /** @var string */ public $element = 'mahnung'; /** @var string */ public $table_element = 'mahnung_mahnung'; /** @var int */ public $entity; /** @var string */ public $ref; /** @var int */ public $fk_facture; /** @var int */ public $fk_soc; /** @var int 1, 2, 3 */ public $stufe; /** @var int Unix-Zeit */ public $date_mahnung; /** @var int Unix-Zeit */ public $date_lim_reglement_alt; /** @var int Unix-Zeit */ public $date_lim_reglement_neu; /** @var float */ public $betrag_offen = 0; /** @var float */ public $mahngebuehr = 0; /** @var float */ public $pauschale_b2b = 0; /** @var float */ public $verzugszinsen = 0; /** @var float */ public $summe_mahnung = 0; /** @var string pdf|mail|druck|none */ public $versandart = self::VERSAND_PDF; /** @var string B2C|B2B */ public $customertype; /** @var float */ public $basiszins_snapshot; /** @var string */ public $pdf_path; /** @var string */ public $note_private; /** @var string Aktuelles Dokumentenmodell */ public $model_pdf; /** @var int 0..9 */ public $status = self::STATUS_ENTWURF; /** @var int Unix-Zeit — Versanddatum (per Hand erfasst oder beim Mail-Versand) */ public $date_versand; /** @var string post|einschreiben|dhl|dpd|hermes|ups|fax|email|persoenlich|eigen */ public $versandweg; /** @var string Rohe Sendungsnummer wie ausgedruckt */ public $tracking_nr; /** @var string dhl|dpag|hermes|dpd|ups|custom — für URL-Template */ public $tracking_provider; /** @var int Unix-Zeit */ public $datec; /** @var int Unix-Zeit */ public $tms; /** @var int */ public $fk_user_creat; /** @var int */ public $fk_user_modif; /** * @param DoliDB $db Datenbank-Handler */ public function __construct($db) { global $conf; $this->db = $db; $this->entity = $conf->entity; } /** * Nächste freie Mahnungs-Referenz für das aktuelle Jahr. * Format: MAHN- * * @return string */ public function getNextRef() { $year = (int) dol_print_date(dol_now(), '%Y'); $prefix = 'MAHN'.$year.'-'; $sql = "SELECT MAX(CAST(SUBSTRING(ref, ".(strlen($prefix) + 1).") AS UNSIGNED)) AS maxnum"; $sql .= " FROM ".MAIN_DB_PREFIX."mahnung_mahnung"; $sql .= " WHERE entity = ".((int) $this->entity); $sql .= " AND ref LIKE '".$this->db->escape($prefix)."%'"; $resql = $this->db->query($sql); $next = 1; if ($resql) { $obj = $this->db->fetch_object($resql); $next = ((int) $obj->maxnum) + 1; $this->db->free($resql); } return $prefix.str_pad((string) $next, 4, '0', STR_PAD_LEFT); } /** * Mahnvorgang anlegen. * * @param User $user Anlegender User * @return int <0 bei Fehler, sonst neue rowid */ public function create($user) { $now = dol_now(); if (empty($this->ref)) { $this->ref = $this->getNextRef(); } $this->db->begin(); $sql = "INSERT INTO ".MAIN_DB_PREFIX."mahnung_mahnung ("; $sql .= "entity, ref, fk_facture, fk_soc, stufe, date_mahnung,"; $sql .= " date_lim_reglement_alt, date_lim_reglement_neu,"; $sql .= " betrag_offen, mahngebuehr, pauschale_b2b, verzugszinsen, summe_mahnung,"; $sql .= " versandart, customertype, basiszins_snapshot, pdf_path, note_private,"; $sql .= " status, datec, fk_user_creat"; $sql .= ") VALUES ("; $sql .= ((int) $this->entity).","; $sql .= "'".$this->db->escape($this->ref)."',"; $sql .= ((int) $this->fk_facture).","; $sql .= ((int) $this->fk_soc).","; $sql .= ((int) $this->stufe).","; $sql .= "'".$this->db->idate($this->date_mahnung ?: $now)."',"; $sql .= ($this->date_lim_reglement_alt ? "'".$this->db->idate($this->date_lim_reglement_alt)."'" : "NULL").","; $sql .= ($this->date_lim_reglement_neu ? "'".$this->db->idate($this->date_lim_reglement_neu)."'" : "NULL").","; $sql .= ((float) $this->betrag_offen).","; $sql .= ((float) $this->mahngebuehr).","; $sql .= ((float) $this->pauschale_b2b).","; $sql .= ((float) $this->verzugszinsen).","; $sql .= ((float) $this->summe_mahnung).","; $sql .= "'".$this->db->escape($this->versandart ?: self::VERSAND_PDF)."',"; $sql .= ($this->customertype ? "'".$this->db->escape($this->customertype)."'" : "NULL").","; $sql .= ($this->basiszins_snapshot !== null ? ((float) $this->basiszins_snapshot) : "NULL").","; $sql .= ($this->pdf_path ? "'".$this->db->escape($this->pdf_path)."'" : "NULL").","; $sql .= ($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").","; $sql .= ((int) $this->status).","; $sql .= "'".$this->db->idate($now)."',"; $sql .= ((int) $user->id); $sql .= ")"; dol_syslog(get_class($this).'::create', LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); $this->db->rollback(); return -1; } $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'mahnung_mahnung'); $this->datec = $now; $this->fk_user_creat = $user->id; $this->db->commit(); return $this->id; } /** * @param int $id rowid * @return int -1 Fehler, 0 nicht gefunden, >0 OK */ public function fetch($id) { $sql = "SELECT t.* FROM ".MAIN_DB_PREFIX."mahnung_mahnung as t"; $sql .= " WHERE t.rowid = ".((int) $id); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); return -1; } if (!$this->db->num_rows($resql)) { $this->db->free($resql); return 0; } $obj = $this->db->fetch_object($resql); $this->id = $obj->rowid; $this->entity = $obj->entity; $this->ref = $obj->ref; $this->fk_facture = $obj->fk_facture; $this->fk_soc = $obj->fk_soc; $this->stufe = $obj->stufe; $this->date_mahnung = $this->db->jdate($obj->date_mahnung); $this->date_lim_reglement_alt = $this->db->jdate($obj->date_lim_reglement_alt); $this->date_lim_reglement_neu = $this->db->jdate($obj->date_lim_reglement_neu); $this->betrag_offen = $obj->betrag_offen; $this->mahngebuehr = $obj->mahngebuehr; $this->pauschale_b2b = $obj->pauschale_b2b; $this->verzugszinsen = $obj->verzugszinsen; $this->summe_mahnung = $obj->summe_mahnung; $this->versandart = $obj->versandart; $this->customertype = $obj->customertype; $this->basiszins_snapshot = $obj->basiszins_snapshot; $this->pdf_path = $obj->pdf_path; $this->note_private = $obj->note_private; $this->status = (int) $obj->status; $this->date_versand = isset($obj->date_versand) ? $this->db->jdate($obj->date_versand) : null; $this->versandweg = $obj->versandweg ?? null; $this->tracking_nr = $obj->tracking_nr ?? null; $this->tracking_provider = $obj->tracking_provider ?? null; $this->datec = $this->db->jdate($obj->datec); $this->tms = $this->db->jdate($obj->tms); $this->fk_user_creat = $obj->fk_user_creat; $this->fk_user_modif = $obj->fk_user_modif; // Alias für CommonObject::fetch_thirdparty() $this->socid = $this->fk_soc; $this->db->free($resql); return 1; } /** * Dokument generieren über das Dolibarr-Dokumentenmodell-System. * * @param string $modele Modellname ('standard_mahnung', 'generic_mahnung_odt') * @param Translate $outputlangs Ausgabesprache * @param int $hidedetails Details ausblenden * @param int $hidedesc Beschreibung ausblenden * @param int $hideref Referenz ausblenden * @param array $moreparams Weitere Parameter * @return int >0 OK, <=0 Fehler */ public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null) { global $conf, $langs; $langs->load("mahnung@mahnung"); if (!dol_strlen($modele)) { $modele = 'standard_mahnung'; if (!empty($this->model_pdf)) { $modele = $this->model_pdf; } elseif (getDolGlobalString('MAHNUNG_ADDON_PDF')) { $modele = getDolGlobalString('MAHNUNG_ADDON_PDF'); } } $modelpath = "core/modules/mahnung/doc/"; return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams); } /** * Mehrere Mahnungen laden. * * @param string $sortfield * @param string $sortorder * @param int $limit * @param int $offset * @param array $filter Schlüssel: fk_facture, fk_soc, stufe, status, ref_like * @param string $mode 'list' | 'count' * @return Mahnung[]|int Liste, Anzahl oder -1 bei Fehler */ public function fetchAll($sortfield = 'date_mahnung', $sortorder = 'DESC', $limit = 0, $offset = 0, $filter = array(), $mode = 'list') { $sql = "SELECT t.rowid FROM ".MAIN_DB_PREFIX."mahnung_mahnung as t"; $sql .= " WHERE t.entity = ".((int) $this->entity); if (!empty($filter['fk_facture'])) { $sql .= " AND t.fk_facture = ".((int) $filter['fk_facture']); } if (!empty($filter['fk_soc'])) { $sql .= " AND t.fk_soc = ".((int) $filter['fk_soc']); } if (isset($filter['stufe']) && $filter['stufe'] !== '') { $sql .= " AND t.stufe = ".((int) $filter['stufe']); } if (isset($filter['status']) && $filter['status'] !== '') { $sql .= " AND t.status = ".((int) $filter['status']); } if (!empty($filter['ref_like'])) { $sql .= " AND t.ref LIKE '%".$this->db->escape($filter['ref_like'])."%'"; } if ($mode === 'count') { $sqlcount = preg_replace('/SELECT t\.rowid/', 'SELECT COUNT(*) as total', $sql); $resqlcount = $this->db->query($sqlcount); if (!$resqlcount) { return -1; } $objcount = $this->db->fetch_object($resqlcount); $this->db->free($resqlcount); return (int) $objcount->total; } $sql .= $this->db->order($sortfield, $sortorder); if ($limit > 0) { $sql .= $this->db->plimit($limit, $offset); } $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); return -1; } $result = array(); while ($obj = $this->db->fetch_object($resql)) { $m = new self($this->db); $m->fetch($obj->rowid); $result[] = $m; } $this->db->free($resql); return $result; } /** * Mahnvorgang aktualisieren. * * @param User $user Bearbeitender User * @return int <0 bei Fehler, sonst id */ public function update($user) { if (empty($this->id)) { $this->error = 'Mahnung::update — id missing'; return -1; } $this->db->begin(); $sql = "UPDATE ".MAIN_DB_PREFIX."mahnung_mahnung SET"; $sql .= " stufe = ".((int) $this->stufe); $sql .= ", date_mahnung = '".$this->db->idate($this->date_mahnung ?: dol_now())."'"; $sql .= ", date_lim_reglement_alt = ".($this->date_lim_reglement_alt ? "'".$this->db->idate($this->date_lim_reglement_alt)."'" : "NULL"); $sql .= ", date_lim_reglement_neu = ".($this->date_lim_reglement_neu ? "'".$this->db->idate($this->date_lim_reglement_neu)."'" : "NULL"); $sql .= ", betrag_offen = ".((float) $this->betrag_offen); $sql .= ", mahngebuehr = ".((float) $this->mahngebuehr); $sql .= ", pauschale_b2b = ".((float) $this->pauschale_b2b); $sql .= ", verzugszinsen = ".((float) $this->verzugszinsen); $sql .= ", summe_mahnung = ".((float) $this->summe_mahnung); $sql .= ", versandart = '".$this->db->escape($this->versandart)."'"; $sql .= ", customertype = ".($this->customertype ? "'".$this->db->escape($this->customertype)."'" : "NULL"); $sql .= ", basiszins_snapshot = ".($this->basiszins_snapshot !== null ? ((float) $this->basiszins_snapshot) : "NULL"); $sql .= ", pdf_path = ".($this->pdf_path ? "'".$this->db->escape($this->pdf_path)."'" : "NULL"); $sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); $sql .= ", status = ".((int) $this->status); $sql .= ", date_versand = ".($this->date_versand ? "'".$this->db->idate($this->date_versand)."'" : "NULL"); $sql .= ", versandweg = ".($this->versandweg ? "'".$this->db->escape($this->versandweg)."'" : "NULL"); $sql .= ", tracking_nr = ".($this->tracking_nr ? "'".$this->db->escape($this->tracking_nr)."'" : "NULL"); $sql .= ", tracking_provider = ".($this->tracking_provider ? "'".$this->db->escape($this->tracking_provider)."'" : "NULL"); $sql .= ", fk_user_modif = ".((int) $user->id); $sql .= " WHERE rowid = ".((int) $this->id); dol_syslog(get_class($this).'::update', LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); $this->db->rollback(); return -1; } $this->fk_user_modif = $user->id; $this->db->commit(); return $this->id; } /** * Mahnvorgang löschen. Verknüpftes PDF wird mitgelöscht falls vorhanden. * * @param User $user Loeschender User * @return int <0 bei Fehler, sonst 1 */ public function delete($user) { $this->db->begin(); if ($this->pdf_path && file_exists($this->pdf_path)) { @unlink($this->pdf_path); } $sql = "DELETE FROM ".MAIN_DB_PREFIX."mahnung_mahnung WHERE rowid = ".((int) $this->id); dol_syslog(get_class($this).'::delete', LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); $this->db->rollback(); return -1; } $this->db->commit(); return 1; } /** * Status auf "erledigt" setzen (Trigger nach Zahlungseingang). * * @param User $user * @return int <0 bei Fehler, sonst 1 */ public function setErledigt($user) { $this->status = self::STATUS_ERLEDIGT; $res = $this->update($user); return $res > 0 ? 1 : -1; } /** * Versand-Daten setzen: Datum, Weg, optional Tracking-Nr/Provider. * Setzt Status automatisch auf VERSENDET, wenn er noch <= ERSTELLT war. * * @param User $user * @param int $dateVersand Unix-Zeit * @param string $weg z.B. 'dhl', 'post', 'einschreiben', ... * @param string|null $trackingNr * @param string|null $trackingProvider * @return int <0 Fehler, sonst 1 */ public function setVersand($user, $dateVersand, $weg, $trackingNr = null, $trackingProvider = null) { $this->date_versand = $dateVersand; $this->versandweg = $weg; $this->tracking_nr = $trackingNr !== null && $trackingNr !== '' ? $trackingNr : null; $this->tracking_provider = $trackingProvider !== null && $trackingProvider !== '' ? $trackingProvider : null; if ((int) $this->status < self::STATUS_VERSENDET) { $this->status = self::STATUS_VERSENDET; } $res = $this->update($user); return $res > 0 ? 1 : -1; } /** * Mapping versandweg → tracking_provider (Default-Provider je Versandweg). * * @param string $weg * @return string|null */ public static function defaultProviderForWeg($weg) { switch ($weg) { case 'dhl': return 'dhl'; case 'einschreiben': return 'dpag'; case 'dpd': return 'dpd'; case 'hermes': return 'hermes'; case 'ups': return 'ups'; default: return null; } } /** * Baut einen Sendungsverfolgungs-Deep-Link aus Provider + Tracking-Nr. * * @param string $provider dhl|dpag|hermes|dpd|ups|custom * @param string $nr * @return string Vollständige URL oder leer bei unbekanntem Provider */ public static function trackingUrl($provider, $nr) { $nr = trim((string) $nr); if ($nr === '') { return ''; } $enc = rawurlencode($nr); switch ((string) $provider) { case 'dhl': return 'https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode='.$enc; case 'dpag': return 'https://www.deutschepost.de/sendung/simpleQuery.html?form.sendungsnummer='.$enc; case 'dpd': return 'https://tracking.dpd.de/status/de_DE/parcel/'.$enc; case 'hermes': return 'https://www.myhermes.de/empfangen/sendungsverfolgung/sendungsinformation/#'.$enc; case 'ups': return 'https://www.ups.com/track?tracknum='.$enc; default: return ''; } } /** * Lokalisiertes Versandweg-Label. * * @param string|null $weg Override (sonst $this->versandweg) * @return string */ public function getVersandwegLabel($weg = null) { global $langs; $w = $weg ?? $this->versandweg; if (empty($w)) { return ''; } $key = 'MahnungVersandweg'.ucfirst(strtolower((string) $w)); $trans = $langs->trans($key); return $trans !== $key ? $trans : (string) $w; } /** * Letzten Mahnvorgang zu einer Rechnung holen (höchste Stufe, neuestes Datum). * * @param int $factureId * @return Mahnung|null */ public function fetchLastByFacture($factureId) { $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."mahnung_mahnung"; $sql .= " WHERE fk_facture = ".((int) $factureId); $sql .= " AND entity = ".((int) $this->entity); $sql .= " AND status NOT IN (".self::STATUS_STORNIERT.")"; $sql .= " ORDER BY stufe DESC, date_mahnung DESC, rowid DESC"; $sql .= " LIMIT 1"; $resql = $this->db->query($sql); if (!$resql || !$this->db->num_rows($resql)) { return null; } $obj = $this->db->fetch_object($resql); $this->db->free($resql); $m = new self($this->db); if ($m->fetch($obj->rowid) > 0) { return $m; } return null; } /** * Verzugszinsen tagesgenau nach BGB §288 berechnen. * Formel: zinsen = betrag_offen * (basiszins + aufschlag) / 100 * tage / 365 * * @param float $betragOffen * @param int $tageVerzug * @param string $kundentyp B2C|B2B * @param float $basiszins in Prozent (z.B. 1.27) * @param float|null $zinssatzOverride Override aus Stufen-Konfig (Prozent) * @return float Zinsen in EUR (gerundet 2 Nachkomma) */ public static function berechneVerzugszinsen($betragOffen, $tageVerzug, $kundentyp, $basiszins, $zinssatzOverride = null) { $tage = max(0, (int) $tageVerzug); if ($tage <= 0 || $betragOffen <= 0) { return 0.0; } if ($zinssatzOverride !== null) { $satz = (float) $zinssatzOverride; } else { $aufschlag = ($kundentyp === self::KUNDENTYP_B2B) ? (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2B', '9.0') : (float) getDolGlobalString('MAHNUNG_AUFSCHLAG_B2C', '5.0'); $satz = (float) $basiszins + $aufschlag; } $zinsen = ((float) $betragOffen) * $satz / 100.0 * $tage / 365.0; return round($zinsen, 2); } /** * Setzt $summe_mahnung = betrag_offen + mahngebuehr + pauschale_b2b + verzugszinsen. * * @return float Neue Summe */ public function rechneSumme() { $this->summe_mahnung = round( (float) $this->betrag_offen + (float) $this->mahngebuehr + (float) $this->pauschale_b2b + (float) $this->verzugszinsen, 2 ); return $this->summe_mahnung; } /** * Lokalisiertes Status-Label. * * @param int|null $status Override (sonst $this->status) * @return string */ public function getStatusLabel($status = null) { global $langs; $s = $status ?? $this->status; switch ((int) $s) { case self::STATUS_ENTWURF: return $langs->trans('MahnungStatusEntwurf'); case self::STATUS_ERSTELLT: return $langs->trans('MahnungStatusErstellt'); case self::STATUS_VERSENDET: return $langs->trans('MahnungStatusVersendet'); case self::STATUS_ERLEDIGT: return $langs->trans('MahnungStatusErledigt'); case self::STATUS_STORNIERT: return $langs->trans('MahnungStatusStorniert'); default: return (string) $s; } } }