diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c40186..391a68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +### Versand & Belege (Mahnungs-Karte) +- Neue Felder `date_versand`, `versandweg`, `tracking_nr`, `tracking_provider` an `llx_mahnung_mahnung` — idempotente Migration laeuft beim ersten Setup-Aufruf nach dem Deploy. +- Neuer Block "Versand & Belege" auf der Mahnungs-Karte: + - Erfassung Versanddatum + Versandweg (Brief/Einschreiben/DHL/DPD/Hermes/UPS/Fax/Mail/Persoenlich/Eigen). + - Optionale Sendungsnummer + Anbieter — Mahnung-Klasse liefert Deep-Link zur Sendungsverfolgung (DHL, Deutsche Post, DPD, Hermes, UPS). + - "Sendung verfolgen"-Button oeffnet die Provider-Seite mit eingesetzter Sendungsnummer. +- Beleg-Upload via Dolibarrs `formfile->showdocuments()` — Dateien landen in `DOL_DATA_ROOT/mahnung//`, voll integriert mit ECM/document.php. +- Status springt automatisch auf `STATUS_VERSENDET` sobald ein Versanddatum gesetzt wird (sofern vorher <= ERSTELLT). +- Neue Methoden `Mahnung::setVersand()`, `Mahnung::trackingUrl()`, `Mahnung::defaultProviderForWeg()`, `Mahnung::getVersandwegLabel()`. + ### Vorschlagsliste — UX - Kunden-Filter: rowid-Input ersetzt durch Dolibarr-Standard `select_company()` (Ajax-Suche bzw. klassisches Dropdown, je nach Dolibarr-Konfiguration). Direkt-Links `?search_socid=74` bleiben funktional. - Neuer Filter "Mindestbetrag" (in EUR, Komma erlaubt). diff --git a/admin/setup.php b/admin/setup.php index 04644cb..68fc542 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -46,6 +46,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnung.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/class/mahnungstufe.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/mahnung/modules_mahnung.php'; +require_once DOL_DOCUMENT_ROOT.'/custom/mahnung/core/modules/modMahnung.class.php'; global $langs, $user, $conf, $db; $langs->loadLangs(array('admin', 'mahnung@mahnung')); @@ -54,6 +55,9 @@ if (!$user->admin && !$user->hasRight('mahnung', 'setup')) { accessforbidden(); } +// Schema-Migration bei jedem Setup-Aufruf (idempotent — fehlende Spalten ergaenzen) +(new modMahnung($db))->migrateVersandFelder(); + $action = GETPOST('action', 'aZ09'); // --------------------------------------------------------------- diff --git a/card.php b/card.php index 9e95bb2..78fc4da 100644 --- a/card.php +++ b/card.php @@ -79,6 +79,41 @@ if ($action === 'regenerate_pdf' && $user->hasRight('mahnung', 'write')) { exit; } +// Versand-Daten speichern (Datum, Weg, optional Tracking) +if ($action === 'set_versand' && $user->hasRight('mahnung', 'write')) { + $y = GETPOSTINT('versand_year'); + $m = GETPOSTINT('versand_month'); + $d = GETPOSTINT('versand_day'); + $dateVersand = ($y && $m && $d) ? dol_mktime(12, 0, 0, $m, $d, $y) : dol_now(); + $weg = GETPOST('versandweg', 'aZ09'); + $trackNr = trim((string) GETPOST('tracking_nr', 'alphanohtml')); + $trackProv = GETPOST('tracking_provider', 'aZ09'); + if (empty($trackProv) && $trackNr !== '') { + $trackProv = Mahnung::defaultProviderForWeg($weg); + } + if ($mahnung->setVersand($user, $dateVersand, $weg, $trackNr ?: null, $trackProv ?: null) > 0) { + setEventMessages($langs->trans('MahnungVersandGespeichert'), null, 'mesgs'); + } else { + setEventMessages($mahnung->error ?: 'Fehler beim Speichern', null, 'errors'); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); + exit; +} + +// Versand-Daten zuruecksetzen (z.B. Korrekturmoeglichkeit) +if ($action === 'clear_versand' && $user->hasRight('mahnung', 'write')) { + $mahnung->date_versand = null; + $mahnung->versandweg = null; + $mahnung->tracking_nr = null; + $mahnung->tracking_provider = null; + // Status nicht zurueckdrehen — nur Daten loeschen + if ($mahnung->update($user) > 0) { + setEventMessages($langs->trans('MahnungVersandGeleert'), null, 'mesgs'); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id)); + exit; +} + llxHeader('', $langs->trans('MahnungRef').' '.$mahnung->ref); print load_fiche_titre($langs->trans('MahnungRef').' '.$mahnung->ref, '', 'fa-envelope-open-text'); @@ -183,7 +218,139 @@ if (!empty($fileList)) { // Aktionen require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php'; +require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php'; $form = new Form($db); +$formfile = new FormFile($db); + +// --- Versand & Belege --- +print '
'; +print load_fiche_titre($langs->trans('MahnungVersandBelege'), '', 'fa-truck'); + +// Versandwege (Dropdown-Optionen, Label kommt aus Lang-File MahnungVersandweg*) +$versandwege = array( + 'post' => $langs->trans('MahnungVersandwegPost'), + 'einschreiben' => $langs->trans('MahnungVersandwegEinschreiben'), + 'dhl' => $langs->trans('MahnungVersandwegDhl'), + 'dpd' => $langs->trans('MahnungVersandwegDpd'), + 'hermes' => $langs->trans('MahnungVersandwegHermes'), + 'ups' => $langs->trans('MahnungVersandwegUps'), + 'fax' => $langs->trans('MahnungVersandwegFax'), + 'email' => $langs->trans('MahnungVersandwegEmail'), + 'persoenlich' => $langs->trans('MahnungVersandwegPersoenlich'), + 'eigen' => $langs->trans('MahnungVersandwegEigen'), +); + +$editVersand = ($action === 'edit_versand') || empty($mahnung->date_versand); +$canWrite = $user->hasRight('mahnung', 'write'); + +if (!empty($mahnung->date_versand) && $action !== 'edit_versand') { + // Anzeige der bereits erfassten Versanddaten + print ''; + print ''; + print ''; + if (!empty($mahnung->tracking_nr)) { + $trackUrl = Mahnung::trackingUrl((string) $mahnung->tracking_provider, (string) $mahnung->tracking_nr); + print ''; + } + print '
'.$langs->trans('MahnungVersanddatum').''.dol_print_date($mahnung->date_versand, 'day').'
'.$langs->trans('MahnungVersandweg').'' + .($mahnung->versandweg && isset($versandwege[$mahnung->versandweg]) ? dol_escape_htmltag($versandwege[$mahnung->versandweg]) : dol_escape_htmltag((string) $mahnung->versandweg)) + .'
'.$langs->trans('MahnungTrackingNr').''; + print ''.dol_escape_htmltag($mahnung->tracking_nr).''; + if (!empty($trackUrl)) { + print ' '; + print img_picto('', 'fa-external-link-alt', 'class="pictofixedwidth"'); + print dol_escape_htmltag($langs->trans('MahnungSendungVerfolgen')).''; + } + print '
'; + if ($canWrite) { + print '
'; + print ''.img_picto('', 'edit').' '.dol_escape_htmltag($langs->trans('MahnungVersandBearbeiten')).' '; + print ''.dol_escape_htmltag($langs->trans('MahnungVersandLeeren')).''; + print '
'; + } +} elseif ($canWrite) { + // Versand-Formular (Erfassung oder Bearbeitung) + $dateInit = !empty($mahnung->date_versand) ? $mahnung->date_versand : dol_now(); + print '
'; + print ''; + print ''; + print ''; + + // Versanddatum + print ''; + + // Versandweg + print ''; + + // Tracking-Nr + print ''; + + // Optional: Tracking-Provider override + print ''; + + print '
'.$langs->trans('MahnungVersanddatum').''; + print $form->selectDate($dateInit, 'versand_', 0, 0, 0, '', 1, 0); + print '
'.$langs->trans('MahnungVersandweg').''; + print ''; + print '
'.$langs->trans('MahnungTrackingNr').''; + print ''; + print ' ('.dol_escape_htmltag($langs->trans('MahnungTrackingProviderAuto')).')'; + print '
'.$langs->trans('MahnungTrackingProvider').''; + print ''; + print '
'; + print '
'; + print ' '; + if (!empty($mahnung->date_versand)) { + print ''.dol_escape_htmltag($langs->trans('Cancel')).''; + } + print '
'; + print '
'; +} + +// --- Sendebelege (Beleg-Upload via Dolibarr-Standard) --- +print '

'.$langs->trans('MahnungSendebelege').'

'; +print '
'.$langs->trans('MahnungSendebelegeHint').'
'; + +$mahnungSafeRef = dol_sanitizeFileName($mahnung->ref); +$mahnungFileDir = (!empty($conf->mahnung->multidir_output[$mahnung->entity]) + ? $conf->mahnung->multidir_output[$mahnung->entity] + : $conf->mahnung->dir_output ?? (DOL_DATA_ROOT.'/mahnung')) + .'/'.$mahnungSafeRef; +// Verzeichnis bei Bedarf anlegen, damit FormFile->showdocuments() das Upload-Formular zeigt +if (!is_dir($mahnungFileDir)) { + dol_mkdir($mahnungFileDir); +} + +$urlSelf = $_SERVER['PHP_SELF'].'?id='.((int) $mahnung->id); +$formfile->showdocuments( + 'mahnung', // $modulepart + $mahnungSafeRef, // $modulesubdir + $mahnungFileDir, // $filedir + $urlSelf, // $urlsource + 0, // $genallowed (kein PDF-Gen-Button hier) + (int) $canWrite, // $delallowed + '', // $modelselected + 1, // $allowgenifempty + 0, // $forcenomultilang + 0, // $iconPDF + 0, // $notused + 0, // $noform + '', // $param + '', // $title + '', // $buttonlabel + '', // $codelang + '', // $morepicto + $mahnung // $object +); if ($mahnung->status !== Mahnung::STATUS_STORNIERT && $user->hasRight('mahnung', 'delete')) { if ($action === 'confirm_storno') { diff --git a/class/mahnung.class.php b/class/mahnung.class.php index 242bb9c..48166ab 100644 --- a/class/mahnung.class.php +++ b/class/mahnung.class.php @@ -98,6 +98,18 @@ class Mahnung extends CommonObject /** @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 — fuer URL-Template */ + public $tracking_provider; + /** @var int Unix-Zeit */ public $datec; @@ -248,6 +260,10 @@ class Mahnung extends CommonObject $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; @@ -386,6 +402,10 @@ class Mahnung extends CommonObject $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); @@ -442,6 +462,96 @@ class Mahnung extends CommonObject 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 Vollstaendige 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 (hoechste Stufe, neuestes Datum). * diff --git a/core/modules/modMahnung.class.php b/core/modules/modMahnung.class.php index 242de86..2593054 100644 --- a/core/modules/modMahnung.class.php +++ b/core/modules/modMahnung.class.php @@ -87,8 +87,12 @@ class modMahnung extends DolibarrModules 'captcha' => 0, ); - // Datenverzeichnisse bei Modul-Aktivierung - $this->dirs = array('/mahnung/temp'); + // Datenverzeichnisse bei Modul-Aktivierung. + // $conf->mahnung->dir_output und multidir_output werden von Dolibarrs + // Conf-Klasse beim Bootstrap automatisch auf DOL_DATA_ROOT/mahnung gesetzt + // (siehe core/class/conf.class.php:744). Damit funktioniert FormFile->showdocuments('mahnung', ...) + // und document.php?modulepart=mahnung out-of-the-box. + $this->dirs = array('/mahnung', '/mahnung/temp'); // Konfigurationsseite $this->config_page_url = array('setup.php@mahnung'); @@ -203,7 +207,7 @@ class modMahnung extends DolibarrModules 'comment' => 'Sucht ueberfaellige Rechnungen, ermittelt vorgeschlagene Mahnstufen, sendet Ntfy-Push', 'frequency' => 1, 'unitfrequency' => 86400, - 'status' => 0, + 'status' => 1, 'test' => 'isModEnabled("mahnung")', 'priority' => 50, ), @@ -312,6 +316,9 @@ class modMahnung extends DolibarrModules return -1; } + // Migration: Versand-Felder ergaenzen, falls Tabelle aus alter Version stammt + $this->migrateVersandFelder(); + // Dokumentenmodelle registrieren $sql = array(); $sql[] = "DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = 'standard_mahnung' AND type = 'mahnung' AND entity = ".((int) $conf->entity); @@ -337,4 +344,36 @@ class modMahnung extends DolibarrModules $sql = array(); return $this->_remove($sql, $options); } + + /** + * Ergaenzt Versand- und Tracking-Felder an llx_mahnung_mahnung, wenn sie + * in einer aelteren Schema-Version noch fehlen. Idempotent — fehlende + * Spalten werden geprueft via SHOW COLUMNS und nur dann hinzugefuegt. + * + * @return void + */ + public function migrateVersandFelder() + { + global $db; + + $alter = array(); + $cols = array( + 'date_versand' => "ADD COLUMN date_versand DATETIME NULL", + 'versandweg' => "ADD COLUMN versandweg VARCHAR(30) NULL", + 'tracking_nr' => "ADD COLUMN tracking_nr VARCHAR(50) NULL", + 'tracking_provider' => "ADD COLUMN tracking_provider VARCHAR(20) NULL", + ); + foreach ($cols as $col => $clause) { + $res = $db->query("SHOW COLUMNS FROM ".MAIN_DB_PREFIX."mahnung_mahnung LIKE '".$db->escape($col)."'"); + if ($res && $db->num_rows($res) == 0) { + $alter[] = $clause; + } + if ($res) { + $db->free($res); + } + } + if (!empty($alter)) { + $db->query("ALTER TABLE ".MAIN_DB_PREFIX."mahnung_mahnung ".implode(', ', $alter)); + } + } } diff --git a/langs/de_DE/mahnung.lang b/langs/de_DE/mahnung.lang index 0052b0e..1af1875 100644 --- a/langs/de_DE/mahnung.lang +++ b/langs/de_DE/mahnung.lang @@ -59,6 +59,35 @@ MahnungVersandMail = E-Mail MahnungVersandDruck = Sammelbrief-Druck MahnungVersandNone = Kein Versand +# +# Versand & Belege (Phase 2) +# +MahnungVersandBelege = Versand & Belege +MahnungVersanddatum = Versanddatum +MahnungVersandweg = Versandweg +MahnungVersandwegPost = Brief (Post) +MahnungVersandwegEinschreiben = Einschreiben (Deutsche Post) +MahnungVersandwegDhl = DHL Paket +MahnungVersandwegDpd = DPD +MahnungVersandwegHermes = Hermes +MahnungVersandwegUps = UPS +MahnungVersandwegFax = Fax +MahnungVersandwegEmail = E-Mail +MahnungVersandwegPersoenlich = Persoenliche Uebergabe +MahnungVersandwegEigen = Eigener Versand +MahnungTrackingNr = Sendungsnummer +MahnungTrackingNrHint = z.B. 1234567890 (DHL), RR123456789DE (Einschreiben) +MahnungTrackingProvider = Anbieter +MahnungTrackingProviderAuto = automatisch erkennen +MahnungSendungVerfolgen = Sendung verfolgen +MahnungVersandBearbeiten = Versand bearbeiten +MahnungVersandLeeren = Versand zuruecksetzen +MahnungVersandLeerenConfirm = Versanddaten wirklich zuruecksetzen? Status bleibt unveraendert. +MahnungVersandGespeichert = Versanddaten gespeichert +MahnungVersandGeleert = Versanddaten zurueckgesetzt +MahnungSendebelege = Sendebelege +MahnungSendebelegeHint = Hier Beleg von Post/DHL/Fax/Mail hochladen (PDF, Foto). Bleibt am Mahnvorgang fuer spaetere Nachweise. + # # Liste / Karte # diff --git a/langs/en_US/mahnung.lang b/langs/en_US/mahnung.lang index fa45a61..e296c48 100644 --- a/langs/en_US/mahnung.lang +++ b/langs/en_US/mahnung.lang @@ -59,6 +59,35 @@ MahnungVersandMail = E-mail MahnungVersandDruck = Bulk print letter MahnungVersandNone = No dispatch +# +# Shipment & receipts (Phase 2) +# +MahnungVersandBelege = Shipment & receipts +MahnungVersanddatum = Shipment date +MahnungVersandweg = Shipment method +MahnungVersandwegPost = Letter (regular mail) +MahnungVersandwegEinschreiben = Registered mail (Deutsche Post) +MahnungVersandwegDhl = DHL parcel +MahnungVersandwegDpd = DPD +MahnungVersandwegHermes = Hermes +MahnungVersandwegUps = UPS +MahnungVersandwegFax = Fax +MahnungVersandwegEmail = E-mail +MahnungVersandwegPersoenlich = Hand delivery +MahnungVersandwegEigen = Own delivery +MahnungTrackingNr = Tracking number +MahnungTrackingNrHint = e.g. 1234567890 (DHL), RR123456789DE (Registered) +MahnungTrackingProvider = Provider +MahnungTrackingProviderAuto = auto-detect +MahnungSendungVerfolgen = Track shipment +MahnungVersandBearbeiten = Edit shipment +MahnungVersandLeeren = Reset shipment data +MahnungVersandLeerenConfirm = Really reset shipment data? Status will remain unchanged. +MahnungVersandGespeichert = Shipment data saved +MahnungVersandGeleert = Shipment data reset +MahnungSendebelege = Shipment receipts +MahnungSendebelegeHint = Upload receipt from postal carrier/DHL/fax/mail (PDF or photo). Stays attached to the dunning case for later verification. + # # List / card # diff --git a/sql/llx_mahnung_mahnung.sql b/sql/llx_mahnung_mahnung.sql index b41261d..037003d 100644 --- a/sql/llx_mahnung_mahnung.sql +++ b/sql/llx_mahnung_mahnung.sql @@ -28,6 +28,10 @@ CREATE TABLE llx_mahnung_mahnung ( pdf_path VARCHAR(255), note_private TEXT, status TINYINT DEFAULT 0 NOT NULL, + date_versand DATETIME, + versandweg VARCHAR(30), + tracking_nr VARCHAR(50), + tracking_provider VARCHAR(20), datec DATETIME, tms TIMESTAMP, fk_user_creat INTEGER,