All checks were successful
Deploy mahnung / deploy (push) Successful in 14s
Vollstaendiges 3-stufiges Mahnwesen nach BGB §288: - SQL-Schema (llx_mahnung_mahnung, llx_mahnung_stufe) - CRUD-Klassen (Mahnung, MahnungStufe, MahnungVorschlag) - TCPDF DIN-5008 PDF-Generierung - Verzugszinsberechnung B2C/B2B + §288 Abs.5 Pauschale - Trigger: offene Mahnungen bei Zahlungseingang schliessen - Hook: Tab + Button auf Rechnungs-/Kundenkarte - Cron: taegl. Vorschlagsliste + Ntfy-Push - Deploy-Pipeline (.forgejo/workflows/deploy.yml) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
504 lines
14 KiB
PHP
504 lines
14 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 fuer Mahnvorgaenge (llx_mahnung_mahnung).
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
|
|
|
/**
|
|
* Klasse Mahnung — repraesentiert 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 int 0..9 */
|
|
public $status = self::STATUS_ENTWURF;
|
|
|
|
/** @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;
|
|
}
|
|
|
|
/**
|
|
* Naechste freie Mahnungs-Referenz fuer 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->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;
|
|
|
|
$this->db->free($resql);
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Mehrere Mahnungen laden.
|
|
*
|
|
* @param string $sortfield
|
|
* @param string $sortorder
|
|
* @param int $limit
|
|
* @param int $offset
|
|
* @param array $filter Schluessel: 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 .= ", 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 loeschen. Verknuepftes PDF wird mitgeloescht 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;
|
|
}
|
|
|
|
/**
|
|
* Letzten Mahnvorgang zu einer Rechnung holen (hoechste 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;
|
|
}
|
|
}
|
|
}
|