diff --git a/ChangeLog.md b/ChangeLog.md index 6221782..c5bb05a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,8 @@ # CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) -## 1.6.2 (14/12/2025) +## 1.6.2 (02/01/2026) +- Correctif : ajout de TimesheetWeek::initAsSpecimen() pour éviter une erreur fatale lors de l’aperçu du modèle de numérotation “advanced”. / Fix: add missing TimesheetWeek::initAsSpecimen() to prevent a fatal error when previewing the “advanced” numbering model. +- Correctif : prise en charge de action=updateMask dans la page de configuration pour enregistrer correctement le masque “advanced” (TIMESHEETWEEK_ADVANCED_MASK). / Fix: handle action=updateMask in setup page to correctly save the “advanced” mask (TIMESHEETWEEK_ADVANCED_MASK). - Envoie les notifications de changement d'état avec des versions HTML pour conserver les actions et caractères spéciaux. / Sends status change notifications with HTML variants to preserve actions and special characters. - Décode les entités HTML dans les objets générés pour afficher correctement les accents dans les courriels. / Decodes HTML entities in generated subjects to display accented characters correctly in emails. - Calcule la signature des notifications en utilisant MAIN_APPLICATION_TITLE ou, à défaut, le nom de la société. / Builds notification signatures using MAIN_APPLICATION_TITLE or, if unavailable, the company name. diff --git a/admin/setup.php b/admin/setup.php index 5835354..40c46e3 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -286,7 +286,7 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Verify CSRF token when the request changes the configuration. -if (in_array($action, array('setmodule', 'setdoc', 'setdocmodel', 'delmodel', 'setquarterday', 'savereminder', 'testreminder'), true)) { +if (in_array($action, array('setmodule', 'updateMask', 'setdoc', 'setdocmodel', 'delmodel', 'setquarterday', 'savereminder', 'testreminder'), true)) { if (function_exists('dol_verify_token')) { if (empty($token) || dol_verify_token($token) <= 0) { accessforbidden(); @@ -304,6 +304,22 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } } +// EN: Persist mask configuration for numbering models (action=updateMask). +if ($action === 'updateMask') { + $maskconst = GETPOST('maskconst', 'alphanohtml'); + $maskconst = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $maskconst); + $maskvalue = GETPOST('maskvalue', 'nohtml'); // Allow mask tokens like {yy}{mm}... + if (!empty($maskconst)) { + $result = dolibarr_set_const($db, $maskconst, $maskvalue, 'chaine', 0, '', $conf->entity); + if ($result > 0) { + setEventMessages($langs->trans('SetupSaved'), null, 'mesgs'); + } else { + setEventMessages($langs->trans('Error'), null, 'errors'); + } + } +} + + // EN: Set the default PDF model while ensuring the model is enabled. if ($action === 'setdoc' && !empty($value)) { $res = timesheetweekEnableDocumentModel($value, $docLabel, $scanDir); @@ -737,4 +753,4 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print dol_get_fiche_end(); llxFooter(); -$db->close(); +$db->close(); \ No newline at end of file diff --git a/class/timesheetweek.class.php b/class/timesheetweek.class.php index 1999907..1ac422b 100644 --- a/class/timesheetweek.class.php +++ b/class/timesheetweek.class.php @@ -90,6 +90,36 @@ public function __construct($db) $this->dir_output = DOL_DATA_ROOT.'/timesheetweek'; } } + /** + * Initialise un objet specimen (prévisualisation / exemple de numérotation). + * + * @return int 1 si OK, <0 si KO + */ + public function initAsSpecimen() + { + $ret = 1; + + // CommonObject (Dolibarr) fournit généralement initAsSpecimenCommon() + if (method_exists($this, 'initAsSpecimenCommon')) { + $ret = $this->initAsSpecimenCommon(); + if ($ret < 0) return $ret; + } + + $now = dol_now(); + + $this->id = 0; + $this->ref = 'TSW-SPECIMEN'; + $this->status = self::STATUS_DRAFT; + + // Utilisé par le modèle de numérotation (get_next_value) via $object->date_creation + $this->date_creation = $now; + + // Valeurs cohérentes si le masque exploite l'année / semaine + $this->year = (int) dol_print_date($now, '%Y'); + $this->week = (int) dol_print_date($now, '%V'); + + return 1; + } /** * EN: Detect lazily if the database schema already stores the PDF model. @@ -1596,6 +1626,11 @@ protected function sendAutomaticNotification($triggerCode, User $actionUser) // EN: Build the direct link to the card so it can be injected inside the e-mail template. $url = dol_buildpath('/timesheetweek/timesheetweek_card.php', 2).'?id='.(int) $this->id; + // FR: Conserve aussi une version HTML cliquable du lien. + // EN: Keep a clickable HTML version of the link as well. + $urlRaw = $url; + $urlHtml = ''.dol_escape_htmltag($urlRaw).''; + $employeeName = $employee ? $employee->getFullName($langs) : ''; $validatorName = $validator ? $validator->getFullName($langs) : ''; $actionUserName = $actionUser->getFullName($langs); @@ -1606,7 +1641,8 @@ protected function sendAutomaticNotification($triggerCode, User $actionUser) '__TIMESHEETWEEK_REF__' => $this->ref, '__TIMESHEETWEEK_WEEK__' => $this->week, '__TIMESHEETWEEK_YEAR__' => $this->year, - '__TIMESHEETWEEK_URL__' => $url, + '__TIMESHEETWEEK_URL__' => $urlHtml, + '__TIMESHEETWEEK_URL_RAW__' => $urlRaw, '__TIMESHEETWEEK_EMPLOYEE_FULLNAME__' => $employeeName, '__TIMESHEETWEEK_VALIDATOR_FULLNAME__' => $validatorName, '__ACTION_USER_FULLNAME__' => $actionUserName, @@ -1828,6 +1864,16 @@ public function sendNativeMailNotification($triggerCode, User $actionUser, $reci $htmlMessage = isset($options['message_html']) ? (string) $options['message_html'] : $message; $isHtml = !empty($options['ishtml']) ? 1 : 0; + if (empty($options['message_html'])) { + $htmlMessage = dol_nl2br(dol_escape_htmltag($message)); + } else { + $htmlMessage = (string) $options['message_html']; + } + + if (!empty($conf->global->MAIN_MAIL_USE_MULTI_PART) || $isHtml) { + $isHtml = 1; + } + $payload = array( 'trigger' => $triggerCode, 'action' => $triggerCode, diff --git a/core/triggers/interface_99_modTimesheetWeek_TimesheetWeekTriggers.class.php b/core/triggers/interface_99_modTimesheetWeek_TimesheetWeekTriggers.class.php index 28ccf96..a80b812 100644 --- a/core/triggers/interface_99_modTimesheetWeek_TimesheetWeekTriggers.class.php +++ b/core/triggers/interface_99_modTimesheetWeek_TimesheetWeekTriggers.class.php @@ -417,7 +417,48 @@ protected function sendNotification($action, TimesheetWeek $timesheet, User $act $sendto = implode(',', array_unique(array_filter($sendtoList))); $cc = implode(',', array_unique(array_filter($ccList))); $bcc = implode(',', array_unique(array_filter($bccList))); - $messageHtml = !empty($template) ? $message : dol_nl2br($message); + // Normalize escaped newlines coming from lang/templates ("\\n" -> "\n") + $normalizeNewlines = function ($str) { + if ($str === null) return $str; + + // Handle double-escaped sequences first + $str = str_replace(array('\\\\r\\\\n', '\\\\n', '\\\\r'), array("\r\n", "\n", "\r"), $str); + $str = preg_replace('/\\\\+r\\\\+n/', "\r\n", $str); + $str = preg_replace('/\\\\+n/', "\n", $str); + $str = preg_replace('/\\\\+r/', "\r", $str); + + // Then handle standard escaped sequences + return str_replace(array('\\r\\n', '\\n', '\\r'), array("\r\n", "\n", "\r"), $str); + }; + + $messageText = $normalizeNewlines($message); + $messageText = str_replace(array("\\r\\n", "\\n", "\\r"), array("\r\n", "\n", "\r"), $messageText); + + // Build HTML part from message (keep existing HTML if message already contains tags) + if (function_exists('dol_textishtml') && dol_textishtml($messageText)) { + $messageHtml = $messageText; + } else { + $messageHtml = dol_nl2br(dol_escape_htmltag($messageText)); + } + + // Make URL clickable in HTML part using Dolibarr helper (no regex linkify). + if (!empty($url) && function_exists('dol_print_url') && strpos($messageHtml, '', '
', ''), $messageHtml); + + dol_syslog( + __METHOD__. + ': prepare mail action='.$action. + ' msgishtml=1 textlen='.(function_exists('dol_strlen') ? dol_strlen($messageText) : strlen($messageText)). + ' htmllen='.(function_exists('dol_strlen') ? dol_strlen($messageHtml) : strlen($messageHtml)). + ' haslink='.((strpos($messageHtml, '
id.'-'.$action.'-'.($recipient ? (int) $recipient->id : 0); $isHtml = 1; @@ -430,7 +471,7 @@ protected function sendNotification($action, TimesheetWeek $timesheet, User $act $substitutions, array( 'subject' => $subject, - 'message' => $message, + 'message' => $messageText, 'message_html' => $messageHtml, 'sendto' => $sendto, 'cc' => $cc,