mahnung/class/mahnung.class.php
Eduard Wisch 10cf41a687
All checks were successful
Deploy mahnung / deploy (push) Successful in 14s
i18n: Alle Texte über $langs->trans() — ~100 neue Sprachschlüssel de_DE + en_US [deploy]
Umlaute in allen lang-Dateien korrigiert. Alle hardcodierten deutschen Strings
in 22 PHP-Dateien durch $langs->trans('Key') ersetzt. Neue Schlüssel für
Cron-Meldungen, Dokument-Aktionen, Bonität, Vorschlag-Status, Template-Vars u.a.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 16:25:50 +02:00

651 lines
19 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* 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<YYYY>-<NNNN>
*
* @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;
}
}
}