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,