From 38587bd95cba7984f1b3ec96880ace7a90cf1a54 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:27:23 +0100 Subject: [PATCH 1/8] Add weekly reminder cron task --- admin/setup.php | 175 +++++++++++++++++++- class/timesheetweek_reminder.class.php | 202 ++++++++++++++++++++++++ core/modules/modTimesheetWeek.class.php | 28 ++-- langs/en_US/timesheetweek.lang | 16 ++ langs/fr_FR/timesheetweek.lang | 16 ++ 5 files changed, 419 insertions(+), 18 deletions(-) create mode 100644 class/timesheetweek_reminder.class.php diff --git a/admin/setup.php b/admin/setup.php index 1394de5..c462753 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -40,6 +40,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php'; // EN: Load document helper functions required for model toggles. // FR: Charge les fonctions d'aide aux documents nécessaires aux commutateurs de modèles. require_once DOL_DOCUMENT_ROOT.'/core/lib/doc.lib.php'; @@ -65,6 +66,13 @@ // FR: Capture les paramètres additionnels utilisés par les bascules de modèles de document Dolibarr. $docLabel = GETPOST('label', 'alphanohtml'); $scanDir = GETPOST('scan_dir', 'alpha'); +$reminderAction = GETPOST('reminder_action', 'aZ09'); + +if (is_readable(DOL_DOCUMENT_ROOT.'/core/class/cemailtemplates.class.php')) { + dol_include_once('/core/class/cemailtemplates.class.php'); +} elseif (is_readable(DOL_DOCUMENT_ROOT.'/core/class/emailtemplates.class.php')) { + dol_include_once('/core/class/emailtemplates.class.php'); +} // EN: Helper to enable a PDF model in the database. // FR: Aide pour activer un modèle PDF dans la base. @@ -277,6 +285,14 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a // EN: Verify CSRF token when the request changes the configuration. // FR: Vérifie le jeton CSRF lorsque la requête modifie la configuration. if (in_array($action, array('setmodule', 'setdoc', 'setdocmodel', 'delmodel', 'setquarterday'), true)) { + if (function_exists('dol_verify_token')) { + if (empty($token) || dol_verify_token($token) <= 0) { + accessforbidden(); + } + } +} + +if ($reminderAction === 'savereminder') { if (function_exists('dol_verify_token')) { if (empty($token) || dol_verify_token($token) <= 0) { accessforbidden(); @@ -337,10 +353,10 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a // EN: Enable or disable the quarter-day selector for daily rate contracts. // FR: Active ou désactive le sélecteur quart de jour pour les contrats au forfait jour. if ($action === 'setquarterday') { - $targetValue = (int) GETPOST('value', 'int'); - if ($targetValue !== 0) { - $targetValue = 1; - } + $targetValue = (int) GETPOST('value', 'int'); + if ($targetValue !== 0) { + $targetValue = 1; + } $res = dolibarr_set_const($db, 'TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', $targetValue, 'chaine', 0, '', $conf->entity); if ($res > 0) { setEventMessages($langs->trans('SetupSaved'), null, 'mesgs'); @@ -349,11 +365,56 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } } +if ($reminderAction === 'savereminder') { + $reminderEnabledValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_ENABLED', 'int'); + $reminderWeekdayValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_WEEKDAY', 'int'); + $reminderHourValue = trim(GETPOST('TIMESHEETWEEK_REMINDER_HOUR', 'alphanohtml')); + $reminderTemplateValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', 'int'); + + $error = 0; + + if ($reminderWeekdayValue < 1 || $reminderWeekdayValue > 7) { + setEventMessages($langs->trans('TimesheetWeekReminderWeekdayInvalid'), null, 'errors'); + $error++; + } + + if (!preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $reminderHourValue)) { + setEventMessages($langs->trans('TimesheetWeekReminderHourInvalid'), null, 'errors'); + $error++; + } + + if (!$error) { + $results = array(); + $results[] = dolibarr_set_const($db, 'TIMESHEETWEEK_REMINDER_ENABLED', ($reminderEnabledValue ? 1 : 0), 'chaine', 0, '', $conf->entity); + $results[] = dolibarr_set_const($db, 'TIMESHEETWEEK_REMINDER_WEEKDAY', $reminderWeekdayValue, 'chaine', 0, '', $conf->entity); + $results[] = dolibarr_set_const($db, 'TIMESHEETWEEK_REMINDER_HOUR', $reminderHourValue, 'chaine', 0, '', $conf->entity); + $results[] = dolibarr_set_const($db, 'TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', $reminderTemplateValue, 'chaine', 0, '', $conf->entity); + + $hasError = false; + foreach ($results as $resultValue) { + if ($resultValue <= 0) { + $hasError = true; + break; + } + } + + if ($hasError) { + setEventMessages($langs->trans('Error'), null, 'errors'); + } else { + setEventMessages($langs->trans('SetupSaved'), null, 'mesgs'); + } + } +} + // EN: Read the selected options so we can highlight them in the UI. // FR: Lit les options sélectionnées pour les mettre en avant dans l'interface. $selectedNumbering = getDolGlobalString('TIMESHEETWEEK_ADDON', 'mod_timesheetweek_standard'); $defaultPdf = getDolGlobalString('TIMESHEETWEEK_ADDON_PDF', 'standard_timesheetweek'); $useQuarterDaySelector = getDolGlobalInt('TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', 0); +$reminderEnabled = getDolGlobalInt('TIMESHEETWEEK_REMINDER_ENABLED', 0); +$reminderWeekday = getDolGlobalInt('TIMESHEETWEEK_REMINDER_WEEKDAY', 1); +$reminderHour = getDolGlobalString('TIMESHEETWEEK_REMINDER_HOUR', '18:00'); +$reminderTemplateId = getDolGlobalInt('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', 0); $directories = array_merge(array('/'), (array) $conf->modules_parts['models']); // EN: Prepare a lightweight object to test numbering module activation. @@ -377,6 +438,26 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a $numberingModules = timesheetweekListNumberingModules($directories, $langs, $sampleTimesheet, $selectedNumbering); $documentModels = timesheetweekListDocumentModels($directories, $langs, $enabledModels, $defaultPdf); $pageToken = function_exists('newToken') ? newToken() : ''; +$form = new Form($db); + +$emailTemplates = array(); +$emailTemplateClass = ''; +if (class_exists('CEmailTemplates')) { + $emailTemplateClass = 'CEmailTemplates'; +} elseif (class_exists('EmailTemplates')) { + $emailTemplateClass = 'EmailTemplates'; +} + +if (!empty($emailTemplateClass)) { + $emailTemplateObject = new $emailTemplateClass($db); + if (method_exists($emailTemplateObject, 'fetchAll')) { + $filters = array('entity' => $conf->entity); + $templatesResult = $emailTemplateObject->fetchAll('', '', 0, 0, $filters); + if (is_array($templatesResult)) { + $emailTemplates = $templatesResult; + } + } +} $title = $langs->trans('ModuleSetup', 'Timesheetweek'); $helpurl = ''; @@ -488,6 +569,92 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print '
'; +print load_fiche_titre($langs->trans('TimesheetWeekReminderSectionTitle'), '', 'email'); +print '
'.$langs->trans('TimesheetWeekReminderSectionHelp').'
'; + +print '
'; +print ''; +print ''; + +print '
'; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; +print ''; + +$weekdayOptions = array( +1 => $langs->trans('Monday'), +2 => $langs->trans('Tuesday'), +3 => $langs->trans('Wednesday'), +4 => $langs->trans('Thursday'), +5 => $langs->trans('Friday'), +6 => $langs->trans('Saturday'), +7 => $langs->trans('Sunday'), +); + +print ''; +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; +print ''; + +$templateOptions = array(0 => $langs->trans('None')); +foreach ($emailTemplates as $templateItem) { + $templateId = 0; + if (!empty($templateItem->id)) { + $templateId = (int) $templateItem->id; + } elseif (!empty($templateItem->rowid)) { + $templateId = (int) $templateItem->rowid; + } + + if (empty($templateId)) { + continue; + } + + $templateLabel = ''; + if (!empty($templateItem->label)) { + $templateLabel = $templateItem->label; + } elseif (!empty($templateItem->ref)) { + $templateLabel = $templateItem->ref; + } elseif (!empty($templateItem->topic)) { + $templateLabel = $templateItem->topic; + } else { + $templateLabel = '#'.$templateId; + } + + $templateOptions[$templateId] = $templateLabel; +} + +print ''; +print ''; +print ''; +print ''; +print ''; + +print '
'.$langs->trans('Name').''.$langs->trans('Description').''.$langs->trans('Value').'
'.$langs->trans('TimesheetWeekReminderEnabled').''.$langs->trans('TimesheetWeekReminderEnabledHelp').''; +print ''; +print '
'.$langs->trans('TimesheetWeekReminderWeekday').''.$langs->trans('TimesheetWeekReminderWeekdayHelp').''.$form->selectarray('TIMESHEETWEEK_REMINDER_WEEKDAY', $weekdayOptions, $reminderWeekday, 0, 0, 0, '', 0, 0, 0, '', '', 1).'
'.$langs->trans('TimesheetWeekReminderHour').''.$langs->trans('TimesheetWeekReminderHourHelp').'
'.$langs->trans('TimesheetWeekReminderEmailTemplate').''.$langs->trans('TimesheetWeekReminderEmailTemplateHelp').''.$form->selectarray('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', $templateOptions, $reminderTemplateId, 0, 0, 0, '', 0, 0, 0, '', '', 1).'
'; +print '
'; + +print '
'; +print '
'; + +print '
'; + print load_fiche_titre($langs->trans('TimesheetWeekPDFModels'), '', 'pdf'); print '
'.$langs->trans('TimesheetWeekPDFModelsHelp').'
'; diff --git a/class/timesheetweek_reminder.class.php b/class/timesheetweek_reminder.class.php new file mode 100644 index 0000000..26f95fd --- /dev/null +++ b/class/timesheetweek_reminder.class.php @@ -0,0 +1,202 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +if (!defined('NOREQUIREUSER')) { + define('NOREQUIREUSER', 1); +} +if (!defined('NOREQUIREDB')) { + define('NOREQUIREDB', 1); +} +if (!defined('NOREQUIRESOC')) { + define('NOREQUIRESOC', 1); +} +if (!defined('NOREQUIRETRAN')) { + define('NOREQUIRETRAN', 1); +} +if (!defined('NOREQUIRESUBPERMS')) { + define('NOREQUIRESUBPERMS', 1); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', 1); +} + +require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.php'; +dol_include_once('/core/class/emailtemplates.class.php'); + +dol_include_once('/timesheetweek/class/timesheetweek.class.php'); + +/** + * Cron helper used to send weekly reminders. + */ +class TimesheetweekReminder +{ + /** + * Run cron job to send weekly reminder emails. + * + * @param DoliDB $db Database handler + * @param int $limit Optional limit for recipients + * @param int $forcerun Force execution (1) or use normal scheduling (0) + * @return int <0 if KO, >=0 if OK (number of emails sent) + */ + public static function run($db, $limit = 0, $forcerun = 0) + { + global $conf, $langs; + + $langs->loadLangs(array('timesheetweek@timesheetweek')); + + dol_syslog(__METHOD__, LOG_DEBUG); + + $reminderEnabled = getDolGlobalInt('TIMESHEETWEEK_REMINDER_ENABLED', 0); + if (empty($reminderEnabled) && empty($forcerun)) { + dol_syslog('TimesheetweekReminder: reminder disabled', LOG_INFO); + return 0; + } + + $reminderWeekday = getDolGlobalInt('TIMESHEETWEEK_REMINDER_WEEKDAY', 1); + if ($reminderWeekday < 1 || $reminderWeekday > 7) { + dol_syslog($langs->trans('TimesheetWeekReminderWeekdayInvalid'), LOG_ERR); + return -1; + } + + $reminderHour = getDolGlobalString('TIMESHEETWEEK_REMINDER_HOUR', '18:00'); + if (!preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $reminderHour)) { + dol_syslog($langs->trans('TimesheetWeekReminderHourInvalid'), LOG_ERR); + return -1; + } + + $templateId = getDolGlobalInt('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', 0); + if (empty($templateId)) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_WARNING); + return -1; + } + + $timezoneCode = !empty($conf->timezone) ? $conf->timezone : 'UTC'; + try { + $timezone = new DateTimeZone($timezoneCode); + } catch (Exception $e) { + dol_syslog($e->getMessage(), LOG_WARNING); + $timezone = new DateTimeZone('UTC'); + } + + $now = dol_now(); + $currentDate = new DateTime('@'.$now); + $currentDate->setTimezone($timezone); + + $currentWeekday = (int) $currentDate->format('N'); + $currentMinutes = ((int) $currentDate->format('H') * 60) + (int) $currentDate->format('i'); + + list($targetHour, $targetMinute) = explode(':', $reminderHour); + $targetMinutes = ((int) $targetHour * 60) + (int) $targetMinute; + + if (empty($forcerun)) { + if ($currentWeekday !== $reminderWeekday) { + dol_syslog('TimesheetweekReminder: not the configured day, skipping execution', LOG_DEBUG); + return 0; + } + if ($currentMinutes < $targetMinutes) { + dol_syslog('TimesheetweekReminder: configured hour not reached, skipping execution', LOG_DEBUG); + return 0; + } + } + + $emailTemplateClass = ''; + if (class_exists('CEmailTemplates')) { + $emailTemplateClass = 'CEmailTemplates'; + } elseif (class_exists('EmailTemplates')) { + $emailTemplateClass = 'EmailTemplates'; + } + + if (empty($emailTemplateClass)) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_ERR); + return -1; + } + + $emailTemplate = new $emailTemplateClass($db); + $templateFetch = $emailTemplate->fetch($templateId); + if ($templateFetch <= 0) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_ERR); + return -1; + } + + $subject = !empty($emailTemplate->topic) ? $emailTemplate->topic : $emailTemplate->label; + $body = !empty($emailTemplate->content) ? $emailTemplate->content : ''; + + if (empty($subject) || empty($body)) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_ERR); + return -1; + } + + $from = getDolGlobalString('MAIN_MAIL_EMAIL_FROM', ''); + if (empty($from)) { + $from = getDolGlobalString('MAIN_INFO_SOCIETE_MAIL', ''); + } + + $substitutions = getCommonSubstitutionArray($langs, 0, null, null, null); + + $sql = 'SELECT rowid, lastname, firstname, email'; + $sql .= ' FROM '.MAIN_DB_PREFIX."user"; + $sql .= " WHERE statut = 1 AND email IS NOT NULL AND email <> ''"; + $sql .= ' AND entity IN (0, '.((int) $conf->entity).')'; + $sql .= ' ORDER BY rowid ASC'; + if ($limit > 0) { + $sql .= $db->plimit((int) $limit); + } + + $resql = $db->query($sql); + if (!$resql) { + dol_syslog($db->lasterror(), LOG_ERR); + return -1; + } + + $emailsSent = 0; + $errors = 0; + + while ($obj = $db->fetch_object($resql)) { + $recipient = trim($obj->email); + if (empty($recipient)) { + continue; + } + + $userSubstitutions = $substitutions; + $userSubstitutions['__USER_FIRSTNAME__'] = $obj->firstname; + $userSubstitutions['__USER_LASTNAME__'] = $obj->lastname; + $userSubstitutions['__USER_FULLNAME__'] = dolGetFirstLastname($obj->firstname, $obj->lastname); + + $preparedSubject = make_substitutions($subject, $userSubstitutions); + $preparedBody = make_substitutions($body, $userSubstitutions); + + $mail = new CMailFile($preparedSubject, $recipient, $from, $preparedBody, array(), array(), array(), '', '', 0, 0, '', '', '', 'utf-8'); + $resultSend = $mail->sendfile(); + if ($resultSend) { + $emailsSent++; + } else { + dol_syslog($langs->trans('TimesheetWeekReminderSendFailed', $recipient), LOG_ERR); + $errors--; + } + } + + $db->free($resql); + + if ($errors < 0) { + return $errors; + } + + return $emailsSent; + } +} diff --git a/core/modules/modTimesheetWeek.class.php b/core/modules/modTimesheetWeek.class.php index c11dd0c..16c79b1 100644 --- a/core/modules/modTimesheetWeek.class.php +++ b/core/modules/modTimesheetWeek.class.php @@ -309,20 +309,20 @@ public function __construct($db) // unit_frequency must be 60 for minute, 3600 for hour, 86400 for day, 604800 for week /* BEGIN MODULEBUILDER CRON */ $this->cronjobs = array( - // 0 => array( - // 'label' => 'MyJob label', - // 'jobtype' => 'method', - // 'class' => '/timesheetweek/class/timesheetweek.class.php', - // 'objectname' => 'TimesheetWeek', - // 'method' => 'doScheduledJob', - // 'parameters' => '', - // 'comment' => 'Comment', - // 'frequency' => 2, - // 'unitfrequency' => 3600, - // 'status' => 0, - // 'test' => 'isModEnabled("timesheetweek")', - // 'priority' => 50, - // ), + 0 => array( + 'label' => 'TimesheetWeekReminderCronLabel', + 'jobtype' => 'method', + 'class' => '/timesheetweek/class/timesheetweek_reminder.class.php', + 'objectname' => 'TimesheetweekReminder', + 'method' => 'run', + 'parameters' => '', + 'comment' => 'TimesheetWeekReminderCronComment', + 'frequency' => 1, + 'unitfrequency' => 86400, + 'status' => 0, + 'test' => 'isModEnabled("timesheetweek")', + 'priority' => 50, + ), ); /* END MODULEBUILDER CRON */ // Example: $this->cronjobs=array( diff --git a/langs/en_US/timesheetweek.lang b/langs/en_US/timesheetweek.lang index abe3a41..727bc29 100644 --- a/langs/en_US/timesheetweek.lang +++ b/langs/en_US/timesheetweek.lang @@ -27,6 +27,22 @@ TimesheetWeekDailyRateOptions = Daily-rate options TimesheetWeekDailyRateOptionsHelp = Configure the helpers available for daily-rate contracts. TimesheetWeekQuarterDayForDailyContract = Quarter-day selector for daily-rate contracts TimesheetWeekQuarterDayForDailyContractHelp = Allow daily-rate employees to declare quarter days instead of half-days only. +TimesheetWeekReminderSectionTitle = Weekly email reminder +TimesheetWeekReminderSectionHelp = Configure the automatic reminder asking users to fill, validate or send their weekly timesheets. +TimesheetWeekReminderEnabled = Enable automatic reminder +TimesheetWeekReminderEnabledHelp = Turn the weekly reminder on or off. +TimesheetWeekReminderWeekday = Reminder day of week +TimesheetWeekReminderWeekdayHelp = Select which day of the week the reminder should be sent. +TimesheetWeekReminderHour = Reminder time (HH:MM) +TimesheetWeekReminderHourHelp = Time of day when the reminder should be sent. +TimesheetWeekReminderHourInvalid = Please enter a valid time formatted as HH:MM. +TimesheetWeekReminderWeekdayInvalid = Please choose a valid day of the week for the reminder. +TimesheetWeekReminderEmailTemplate = Email template for reminder +TimesheetWeekReminderEmailTemplateHelp = Choose the Dolibarr email template used to send the reminder. +TimesheetWeekReminderCronLabel = Weekly timesheet reminder +TimesheetWeekReminderCronComment = Send weekly reminder emails prompting users to complete their timesheets. +TimesheetWeekReminderTemplateMissing = Reminder email template is not configured. +TimesheetWeekReminderSendFailed = Failed to send the reminder email to %s. NewSection=New section TIMESHEETWEEK_MYPARAM1 = My param 1 TIMESHEETWEEK_MYPARAM1Tooltip = My param 1 tooltip diff --git a/langs/fr_FR/timesheetweek.lang b/langs/fr_FR/timesheetweek.lang index e0b0a9b..78d9b38 100644 --- a/langs/fr_FR/timesheetweek.lang +++ b/langs/fr_FR/timesheetweek.lang @@ -27,6 +27,22 @@ TimesheetWeekDailyRateOptions = Options forfait jour TimesheetWeekDailyRateOptionsHelp = Configure les assistants disponibles pour les contrats au forfait jour. TimesheetWeekQuarterDayForDailyContract = Sélecteur quart de jour pour forfait jour TimesheetWeekQuarterDayForDailyContractHelp = Autorise les salariés au forfait jour à déclarer des quarts de jour au lieu des seules demi-journées. +TimesheetWeekReminderSectionTitle = Rappel hebdomadaire par email +TimesheetWeekReminderSectionHelp = Configure le rappel automatique invitant les utilisateurs à saisir, valider ou envoyer leurs feuilles hebdomadaires. +TimesheetWeekReminderEnabled = Activer le rappel automatique +TimesheetWeekReminderEnabledHelp = Active ou désactive l'envoi hebdomadaire du rappel. +TimesheetWeekReminderWeekday = Jour d'envoi du rappel +TimesheetWeekReminderWeekdayHelp = Choisissez le jour de la semaine pour envoyer le rappel. +TimesheetWeekReminderHour = Heure d'envoi (HH:MM) +TimesheetWeekReminderHourHelp = Heure d'envoi du rappel, au format 24 h. +TimesheetWeekReminderHourInvalid = Merci de saisir une heure valide au format HH:MM. +TimesheetWeekReminderWeekdayInvalid = Merci de choisir un jour de semaine valide pour le rappel. +TimesheetWeekReminderEmailTemplate = Modèle d'email pour le rappel +TimesheetWeekReminderEmailTemplateHelp = Sélectionnez le modèle d'email Dolibarr utilisé pour envoyer le rappel. +TimesheetWeekReminderCronLabel = Rappel hebdomadaire des feuilles d'heures +TimesheetWeekReminderCronComment = Envoie un rappel hebdomadaire par email pour inviter les utilisateurs à compléter leurs feuilles d'heures. +TimesheetWeekReminderTemplateMissing = Le modèle d'email de rappel n'est pas configuré. +TimesheetWeekReminderSendFailed = Échec de l'envoi du rappel à %s. NewSection=Nouvelle section TIMESHEETWEEK_MYPARAM1 = Mon paramètre 1 TIMESHEETWEEK_MYPARAM1Tooltip = Info-bulle de mon paramètre 1 From ce6696c86929400489e3c55781038d03dec422e0 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:10:15 +0100 Subject: [PATCH 2/8] =?UTF-8?q?Affiner=20la=20v=C3=A9rification=20horaire?= =?UTF-8?q?=20du=20rappel=20hebdomadaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- class/timesheetweek_reminder.class.php | 53 +++++++++++++------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/class/timesheetweek_reminder.class.php b/class/timesheetweek_reminder.class.php index 26f95fd..22d4a4f 100644 --- a/class/timesheetweek_reminder.class.php +++ b/class/timesheetweek_reminder.class.php @@ -62,55 +62,54 @@ public static function run($db, $limit = 0, $forcerun = 0) dol_syslog(__METHOD__, LOG_DEBUG); - $reminderEnabled = getDolGlobalInt('TIMESHEETWEEK_REMINDER_ENABLED', 0); + $reminderEnabledConst = dolibarr_get_const($db, 'TIMESHEETWEEK_REMINDER_ENABLED', $conf->entity); + $reminderEnabled = !empty($reminderEnabledConst) ? (int) $reminderEnabledConst : 0; if (empty($reminderEnabled) && empty($forcerun)) { dol_syslog('TimesheetweekReminder: reminder disabled', LOG_INFO); return 0; } - $reminderWeekday = getDolGlobalInt('TIMESHEETWEEK_REMINDER_WEEKDAY', 1); + $reminderWeekdayConst = dolibarr_get_const($db, 'TIMESHEETWEEK_REMINDER_WEEKDAY', $conf->entity); + $reminderWeekday = !empty($reminderWeekdayConst) ? (int) $reminderWeekdayConst : 1; if ($reminderWeekday < 1 || $reminderWeekday > 7) { dol_syslog($langs->trans('TimesheetWeekReminderWeekdayInvalid'), LOG_ERR); return -1; } - $reminderHour = getDolGlobalString('TIMESHEETWEEK_REMINDER_HOUR', '18:00'); + $reminderHourConst = dolibarr_get_const($db, 'TIMESHEETWEEK_REMINDER_HOUR', $conf->entity); + $reminderHour = !empty($reminderHourConst) ? $reminderHourConst : '18:00'; if (!preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $reminderHour)) { dol_syslog($langs->trans('TimesheetWeekReminderHourInvalid'), LOG_ERR); return -1; } - $templateId = getDolGlobalInt('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', 0); + $templateIdConst = dolibarr_get_const($db, 'TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', $conf->entity); + $templateId = !empty($templateIdConst) ? (int) $templateIdConst : 0; if (empty($templateId)) { dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_WARNING); - return -1; + return 0; } $timezoneCode = !empty($conf->timezone) ? $conf->timezone : 'UTC'; - try { - $timezone = new DateTimeZone($timezoneCode); - } catch (Exception $e) { - dol_syslog($e->getMessage(), LOG_WARNING); - $timezone = new DateTimeZone('UTC'); - } - $now = dol_now(); - $currentDate = new DateTime('@'.$now); - $currentDate->setTimezone($timezone); - - $currentWeekday = (int) $currentDate->format('N'); - $currentMinutes = ((int) $currentDate->format('H') * 60) + (int) $currentDate->format('i'); + $nowArray = dol_getdate($now, true, $timezoneCode); + $currentWeekday = (int) $nowArray['wday']; + $currentWeekdayIso = ($currentWeekday === 0) ? 7 : $currentWeekday; + $currentMinutes = ((int) $nowArray['hours'] * 60) + (int) $nowArray['minutes']; list($targetHour, $targetMinute) = explode(':', $reminderHour); $targetMinutes = ((int) $targetHour * 60) + (int) $targetMinute; + $windowMinutes = 60; + $lowerBound = max(0, $targetMinutes - $windowMinutes); + $upperBound = min(1440, $targetMinutes + $windowMinutes); if (empty($forcerun)) { - if ($currentWeekday !== $reminderWeekday) { + if ($currentWeekdayIso !== $reminderWeekday) { dol_syslog('TimesheetweekReminder: not the configured day, skipping execution', LOG_DEBUG); return 0; } - if ($currentMinutes < $targetMinutes) { - dol_syslog('TimesheetweekReminder: configured hour not reached, skipping execution', LOG_DEBUG); + if ($currentMinutes < $lowerBound || $currentMinutes > $upperBound) { + dol_syslog('TimesheetweekReminder: outside configured time window, skipping execution', LOG_DEBUG); return 0; } } @@ -130,16 +129,16 @@ public static function run($db, $limit = 0, $forcerun = 0) $emailTemplate = new $emailTemplateClass($db); $templateFetch = $emailTemplate->fetch($templateId); if ($templateFetch <= 0) { - dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_ERR); - return -1; + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_WARNING); + return 0; } $subject = !empty($emailTemplate->topic) ? $emailTemplate->topic : $emailTemplate->label; $body = !empty($emailTemplate->content) ? $emailTemplate->content : ''; if (empty($subject) || empty($body)) { - dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_ERR); - return -1; + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_WARNING); + return 0; } $from = getDolGlobalString('MAIN_MAIL_EMAIL_FROM', ''); @@ -187,14 +186,14 @@ public static function run($db, $limit = 0, $forcerun = 0) $emailsSent++; } else { dol_syslog($langs->trans('TimesheetWeekReminderSendFailed', $recipient), LOG_ERR); - $errors--; + $errors++; } } $db->free($resql); - if ($errors < 0) { - return $errors; + if ($errors > 0) { + return -$errors; } return $emailsSent; From 46faad8487e501f0599ecfd69e13dfcfced65d76 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:20:40 +0100 Subject: [PATCH 3/8] Filter reminder recipients by permissions --- class/timesheetweek_reminder.class.php | 42 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/class/timesheetweek_reminder.class.php b/class/timesheetweek_reminder.class.php index 22d4a4f..2cc6a0b 100644 --- a/class/timesheetweek_reminder.class.php +++ b/class/timesheetweek_reminder.class.php @@ -102,18 +102,18 @@ public static function run($db, $limit = 0, $forcerun = 0) $windowMinutes = 60; $lowerBound = max(0, $targetMinutes - $windowMinutes); $upperBound = min(1440, $targetMinutes + $windowMinutes); - + if (empty($forcerun)) { if ($currentWeekdayIso !== $reminderWeekday) { - dol_syslog('TimesheetweekReminder: not the configured day, skipping execution', LOG_DEBUG); - return 0; + dol_syslog('TimesheetweekReminder: not the configured day, skipping execution', LOG_DEBUG); + return 0; } if ($currentMinutes < $lowerBound || $currentMinutes > $upperBound) { - dol_syslog('TimesheetweekReminder: outside configured time window, skipping execution', LOG_DEBUG); - return 0; + dol_syslog('TimesheetweekReminder: outside configured time window, skipping execution', LOG_DEBUG); + return 0; } } - + $emailTemplateClass = ''; if (class_exists('CEmailTemplates')) { $emailTemplateClass = 'CEmailTemplates'; @@ -147,12 +147,30 @@ public static function run($db, $limit = 0, $forcerun = 0) } $substitutions = getCommonSubstitutionArray($langs, 0, null, null, null); - - $sql = 'SELECT rowid, lastname, firstname, email'; - $sql .= ' FROM '.MAIN_DB_PREFIX."user"; - $sql .= " WHERE statut = 1 AND email IS NOT NULL AND email <> ''"; - $sql .= ' AND entity IN (0, '.((int) $conf->entity).')'; - $sql .= ' ORDER BY rowid ASC'; + + $eligibleRights = array( + 45000301, // read own + 45000302, // read child + 45000303, // read all + 45000304, // write own + 45000305, // write child + 45000306, // write all + 45000310, // validate generic + 45000311, // validate own + 45000312, // validate child + 45000313, // validate all + 45000314, // seal + 45000315, // unseal + ); + + $entityFilter = getEntity('user'); + $sql = 'SELECT DISTINCT u.rowid, u.lastname, u.firstname, u.email'; + $sql .= ' FROM '.MAIN_DB_PREFIX."user AS u"; + $sql .= ' INNER JOIN '.MAIN_DB_PREFIX."user_rights AS ur ON ur.fk_user = u.rowid AND ur.entity IN (".$entityFilter.')'; + $sql .= " WHERE u.statut = 1 AND u.email IS NOT NULL AND u.email <> ''"; + $sql .= ' AND u.entity IN ('.$entityFilter.')'; + $sql .= ' AND ur.fk_id IN ('.implode(',', array_map('intval', $eligibleRights)).')'; + $sql .= ' ORDER BY u.rowid ASC'; if ($limit > 0) { $sql .= $db->plimit((int) $limit); } From 3f241a51e349ec96f33a381cffd877a9a3988354 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:27:28 +0100 Subject: [PATCH 4/8] Handle reminder emails via templates --- class/timesheetweek_reminder.class.php | 20 +++++++++++++++++--- langs/en_US/timesheetweek.lang | 1 + langs/fr_FR/timesheetweek.lang | 1 + 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/class/timesheetweek_reminder.class.php b/class/timesheetweek_reminder.class.php index 2cc6a0b..d734b32 100644 --- a/class/timesheetweek_reminder.class.php +++ b/class/timesheetweek_reminder.class.php @@ -35,8 +35,10 @@ } require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.php'; +require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; dol_include_once('/core/class/emailtemplates.class.php'); dol_include_once('/timesheetweek/class/timesheetweek.class.php'); @@ -147,6 +149,7 @@ public static function run($db, $limit = 0, $forcerun = 0) } $substitutions = getCommonSubstitutionArray($langs, 0, null, null, null); + complete_substitutions_array($substitutions, $langs, null); $eligibleRights = array( 45000301, // read own @@ -190,10 +193,20 @@ public static function run($db, $limit = 0, $forcerun = 0) continue; } + $user = new User($db); + $fetchUser = $user->fetch($obj->rowid); + if ($fetchUser < 0) { + dol_syslog($user->error, LOG_ERR); + $errors++; + continue; + } + $userSubstitutions = $substitutions; - $userSubstitutions['__USER_FIRSTNAME__'] = $obj->firstname; - $userSubstitutions['__USER_LASTNAME__'] = $obj->lastname; - $userSubstitutions['__USER_FULLNAME__'] = dolGetFirstLastname($obj->firstname, $obj->lastname); + $userSubstitutions['__USER_FIRSTNAME__'] = $user->firstname; + $userSubstitutions['__USER_LASTNAME__'] = $user->lastname; + $userSubstitutions['__USER_FULLNAME__'] = dolGetFirstLastname($user->firstname, $user->lastname); + $userSubstitutions['__USER_EMAIL__'] = $recipient; + complete_substitutions_array($userSubstitutions, $langs, null, $user); $preparedSubject = make_substitutions($subject, $userSubstitutions); $preparedBody = make_substitutions($body, $userSubstitutions); @@ -202,6 +215,7 @@ public static function run($db, $limit = 0, $forcerun = 0) $resultSend = $mail->sendfile(); if ($resultSend) { $emailsSent++; + dol_syslog($langs->trans('TimesheetWeekReminderSendSuccess', $recipient), LOG_INFO); } else { dol_syslog($langs->trans('TimesheetWeekReminderSendFailed', $recipient), LOG_ERR); $errors++; diff --git a/langs/en_US/timesheetweek.lang b/langs/en_US/timesheetweek.lang index 727bc29..53e334d 100644 --- a/langs/en_US/timesheetweek.lang +++ b/langs/en_US/timesheetweek.lang @@ -43,6 +43,7 @@ TimesheetWeekReminderCronLabel = Weekly timesheet reminder TimesheetWeekReminderCronComment = Send weekly reminder emails prompting users to complete their timesheets. TimesheetWeekReminderTemplateMissing = Reminder email template is not configured. TimesheetWeekReminderSendFailed = Failed to send the reminder email to %s. +TimesheetWeekReminderSendSuccess = Reminder email successfully sent to %s. NewSection=New section TIMESHEETWEEK_MYPARAM1 = My param 1 TIMESHEETWEEK_MYPARAM1Tooltip = My param 1 tooltip diff --git a/langs/fr_FR/timesheetweek.lang b/langs/fr_FR/timesheetweek.lang index 78d9b38..6727844 100644 --- a/langs/fr_FR/timesheetweek.lang +++ b/langs/fr_FR/timesheetweek.lang @@ -43,6 +43,7 @@ TimesheetWeekReminderCronLabel = Rappel hebdomadaire des feuilles d'heures TimesheetWeekReminderCronComment = Envoie un rappel hebdomadaire par email pour inviter les utilisateurs à compléter leurs feuilles d'heures. TimesheetWeekReminderTemplateMissing = Le modèle d'email de rappel n'est pas configuré. TimesheetWeekReminderSendFailed = Échec de l'envoi du rappel à %s. +TimesheetWeekReminderSendSuccess = Rappel envoyé avec succès à %s. NewSection=Nouvelle section TIMESHEETWEEK_MYPARAM1 = Mon paramètre 1 TIMESHEETWEEK_MYPARAM1Tooltip = Info-bulle de mon paramètre 1 From 8c457c8e0866c061fefcffd4a878975176105aef Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:47:43 +0100 Subject: [PATCH 5/8] Add test reminder email trigger --- admin/setup.php | 27 +++++++++++++++++++------- class/timesheetweek_reminder.class.php | 26 ++++++++++++++++++++----- langs/en_US/timesheetweek.lang | 3 +++ langs/fr_FR/timesheetweek.lang | 3 +++ 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/admin/setup.php b/admin/setup.php index c462753..cfbbe9f 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -46,6 +46,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/doc.lib.php'; dol_include_once('/timesheetweek/lib/timesheetweek.lib.php'); dol_include_once('/timesheetweek/class/timesheetweek.class.php'); +dol_include_once('/timesheetweek/class/timesheetweek_reminder.class.php'); // EN: Load translation files required for the configuration page. // FR: Charge les fichiers de traduction nécessaires à la page de configuration. @@ -292,7 +293,7 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } } -if ($reminderAction === 'savereminder') { +if (in_array($reminderAction, array('savereminder', 'testreminder'), true)) { if (function_exists('dol_verify_token')) { if (empty($token) || dol_verify_token($token) <= 0) { accessforbidden(); @@ -366,9 +367,9 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } if ($reminderAction === 'savereminder') { - $reminderEnabledValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_ENABLED', 'int'); - $reminderWeekdayValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_WEEKDAY', 'int'); - $reminderHourValue = trim(GETPOST('TIMESHEETWEEK_REMINDER_HOUR', 'alphanohtml')); +$reminderEnabledValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_ENABLED', 'int'); +$reminderWeekdayValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_WEEKDAY', 'int'); +$reminderHourValue = trim(GETPOST('TIMESHEETWEEK_REMINDER_HOUR', 'alphanohtml')); $reminderTemplateValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', 'int'); $error = 0; @@ -402,8 +403,17 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a setEventMessages($langs->trans('Error'), null, 'errors'); } else { setEventMessages($langs->trans('SetupSaved'), null, 'mesgs'); +} +} +} + + if ($reminderAction === 'testreminder') { + $resultTest = TimesheetweekReminder::sendTest($db, $user); + if ($resultTest > 0) { + setEventMessages($langs->trans('TimesheetWeekReminderTestSuccess', $resultTest), null, 'mesgs'); + } else { + setEventMessages($langs->trans('TimesheetWeekReminderTestError'), null, 'errors'); } - } } // EN: Read the selected options so we can highlight them in the UI. @@ -574,7 +584,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print '
'; print ''; -print ''; print '
'; print ''; @@ -650,7 +659,11 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print '
'; print '
'; -print '
'; +print '
'; +print ''; +print ' '; +print ''; +print '
'; print '
'; print '
'; diff --git a/class/timesheetweek_reminder.class.php b/class/timesheetweek_reminder.class.php index d734b32..1f15fe1 100644 --- a/class/timesheetweek_reminder.class.php +++ b/class/timesheetweek_reminder.class.php @@ -51,12 +51,13 @@ class TimesheetweekReminder /** * Run cron job to send weekly reminder emails. * - * @param DoliDB $db Database handler - * @param int $limit Optional limit for recipients - * @param int $forcerun Force execution (1) or use normal scheduling (0) - * @return int <0 if KO, >=0 if OK (number of emails sent) + * @param DoliDB $db Database handler + * @param int $limit Optional limit for recipients + * @param int $forcerun Force execution (1) or use normal scheduling (0) + * @param array $targetUserIds Limit execution to specific user ids when provided + * @return int <0 if KO, >=0 if OK (number of emails sent) */ - public static function run($db, $limit = 0, $forcerun = 0) + public static function run($db, $limit = 0, $forcerun = 0, array $targetUserIds = array()) { global $conf, $langs; @@ -173,6 +174,9 @@ public static function run($db, $limit = 0, $forcerun = 0) $sql .= " WHERE u.statut = 1 AND u.email IS NOT NULL AND u.email <> ''"; $sql .= ' AND u.entity IN ('.$entityFilter.')'; $sql .= ' AND ur.fk_id IN ('.implode(',', array_map('intval', $eligibleRights)).')'; + if (!empty($targetUserIds)) { + $sql .= ' AND u.rowid IN ('.implode(',', array_map('intval', $targetUserIds)).')'; + } $sql .= ' ORDER BY u.rowid ASC'; if ($limit > 0) { $sql .= $db->plimit((int) $limit); @@ -230,4 +234,16 @@ public static function run($db, $limit = 0, $forcerun = 0) return $emailsSent; } + + /** + * Send a reminder test email to the current user using the configured template. + * + * @param DoliDB $db Database handler + * @param User $user Current user + * @return int <0 if KO, >=0 if OK (number of emails sent) + */ + public static function sendTest($db, User $user) + { + return self::run($db, 1, 1, array((int) $user->id)); + } } diff --git a/langs/en_US/timesheetweek.lang b/langs/en_US/timesheetweek.lang index 53e334d..d43a10f 100644 --- a/langs/en_US/timesheetweek.lang +++ b/langs/en_US/timesheetweek.lang @@ -44,6 +44,9 @@ TimesheetWeekReminderCronComment = Send weekly reminder emails prompting users t TimesheetWeekReminderTemplateMissing = Reminder email template is not configured. TimesheetWeekReminderSendFailed = Failed to send the reminder email to %s. TimesheetWeekReminderSendSuccess = Reminder email successfully sent to %s. +TimesheetWeekReminderSendTest = Send test email +TimesheetWeekReminderTestSuccess = Test reminder email sent (%s) +TimesheetWeekReminderTestError = Unable to send the test reminder email. NewSection=New section TIMESHEETWEEK_MYPARAM1 = My param 1 TIMESHEETWEEK_MYPARAM1Tooltip = My param 1 tooltip diff --git a/langs/fr_FR/timesheetweek.lang b/langs/fr_FR/timesheetweek.lang index 6727844..047ce7b 100644 --- a/langs/fr_FR/timesheetweek.lang +++ b/langs/fr_FR/timesheetweek.lang @@ -44,6 +44,9 @@ TimesheetWeekReminderCronComment = Envoie un rappel hebdomadaire par email pour TimesheetWeekReminderTemplateMissing = Le modèle d'email de rappel n'est pas configuré. TimesheetWeekReminderSendFailed = Échec de l'envoi du rappel à %s. TimesheetWeekReminderSendSuccess = Rappel envoyé avec succès à %s. +TimesheetWeekReminderSendTest = Envoyer un mail de test +TimesheetWeekReminderTestSuccess = Mail de test envoyé (%s) +TimesheetWeekReminderTestError = Échec de l'envoi du mail de test. NewSection=Nouvelle section TIMESHEETWEEK_MYPARAM1 = Mon paramètre 1 TIMESHEETWEEK_MYPARAM1Tooltip = Info-bulle de mon paramètre 1 From a7909d3efbf9ce23b89094e495898dcbef1d4cee Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:21:00 +0100 Subject: [PATCH 6/8] Remove French comments from setup --- admin/setup.php | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/admin/setup.php b/admin/setup.php index cfbbe9f..7d19608 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -22,7 +22,6 @@ */ // EN: Load Dolibarr environment with fallback paths. -// FR: Charge l'environnement Dolibarr en testant les chemins possibles. $res = 0; if (!$res && file_exists(__DIR__.'/../main.inc.php')) { $res = require_once __DIR__.'/../main.inc.php'; @@ -42,29 +41,24 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php'; // EN: Load document helper functions required for model toggles. -// FR: Charge les fonctions d'aide aux documents nécessaires aux commutateurs de modèles. require_once DOL_DOCUMENT_ROOT.'/core/lib/doc.lib.php'; dol_include_once('/timesheetweek/lib/timesheetweek.lib.php'); dol_include_once('/timesheetweek/class/timesheetweek.class.php'); dol_include_once('/timesheetweek/class/timesheetweek_reminder.class.php'); // EN: Load translation files required for the configuration page. -// FR: Charge les fichiers de traduction nécessaires à la page de configuration. $langs->loadLangs(array('admin', 'other', 'timesheetweek@timesheetweek')); // EN: Only administrators can access the setup. -// FR: Seuls les administrateurs peuvent accéder à la configuration. if (empty($user->admin)) { accessforbidden(); } // EN: Read HTTP parameters once so we can re-use them further down. -// FR: Lit les paramètres HTTP une seule fois pour les réutiliser ensuite. $action = GETPOST('action', 'aZ09'); $value = GETPOST('value', 'alphanohtml'); $token = GETPOST('token', 'alphanohtml'); // EN: Capture additional parameters used to reproduce Dolibarr's document model toggles. -// FR: Capture les paramètres additionnels utilisés par les bascules de modèles de document Dolibarr. $docLabel = GETPOST('label', 'alphanohtml'); $scanDir = GETPOST('scan_dir', 'alpha'); $reminderAction = GETPOST('reminder_action', 'aZ09'); @@ -76,7 +70,6 @@ } // EN: Helper to enable a PDF model in the database. -// FR: Aide pour activer un modèle PDF dans la base. function timesheetweekEnableDocumentModel($model, $label = '', $scandir = '') { global $db, $conf; @@ -86,7 +79,6 @@ function timesheetweekEnableDocumentModel($model, $label = '', $scandir = '') } // EN: Check if the model already exists for the current entity to avoid duplicates. - // FR: Vérifie si le modèle existe déjà pour l'entité courante afin d'éviter les doublons. $sql = 'SELECT rowid FROM '.MAIN_DB_PREFIX."document_model WHERE nom='".$db->escape($model)."' AND type='timesheetweek' AND entity=".((int) $conf->entity); $resql = $db->query($sql); if (!$resql) { @@ -100,13 +92,11 @@ function timesheetweekEnableDocumentModel($model, $label = '', $scandir = '') $fields = array(); // EN: Refresh label when provided to keep UI messages in sync. - // FR: Met à jour le libellé fourni pour garder l'interface cohérente. if ($label !== '') { $fields[] = "libelle='".$db->escape($label)."'"; } // EN: Refresh directory hint when provided to ease future scans. - // FR: Met à jour le chemin fourni pour faciliter les scans ultérieurs. if ($scandir !== '') { $fields[] = "description='".$db->escape($scandir)."'"; } @@ -131,7 +121,6 @@ function timesheetweekEnableDocumentModel($model, $label = '', $scandir = '') // EN: Helper to disable a PDF model from the database. -// FR: Aide pour désactiver un modèle PDF dans la base. function timesheetweekDisableDocumentModel($model) { if (empty($model)) { @@ -143,7 +132,6 @@ function timesheetweekDisableDocumentModel($model) } // EN: Build the list of numbering modules available for the module. -// FR: Construit la liste des modules de numérotation disponibles pour le module. function timesheetweekListNumberingModules(array $directories, Translate $langs, TimesheetWeek $sample, $selected) { global $db; @@ -152,7 +140,6 @@ function timesheetweekListNumberingModules(array $directories, Translate $langs, foreach ($directories as $reldir) { // EN: Resolve the directory that holds the numbering module classes. - // FR: Résout le répertoire qui contient les classes de numérotation. $dir = dol_buildpath(rtrim($reldir, '/').'/timesheetweek/core/modules/timesheetweek/'); if (!is_dir($dir)) { continue; @@ -219,7 +206,6 @@ function timesheetweekListNumberingModules(array $directories, Translate $langs, } // EN: Build the list of available document models for the module. -// FR: Construit la liste des modèles de documents disponibles pour le module. function timesheetweekListDocumentModels(array $directories, Translate $langs, array $enabled, $default) { global $db; @@ -228,7 +214,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a foreach ($directories as $reldir) { // EN: Resolve the directory that stores the document model definitions. - // FR: Résout le répertoire qui contient les définitions de modèles de document. $dir = dol_buildpath(rtrim($reldir, '/').'/timesheetweek/core/modules/timesheetweek/doc/'); if (!is_dir($dir)) { continue; @@ -284,7 +269,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Verify CSRF token when the request changes the configuration. -// FR: Vérifie le jeton CSRF lorsque la requête modifie la configuration. if (in_array($action, array('setmodule', 'setdoc', 'setdocmodel', 'delmodel', 'setquarterday'), true)) { if (function_exists('dol_verify_token')) { if (empty($token) || dol_verify_token($token) <= 0) { @@ -302,7 +286,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Persist the chosen numbering module. -// FR: Enregistre le module de numérotation choisi. if ($action === 'setmodule' && !empty($value)) { $result = dolibarr_set_const($db, 'TIMESHEETWEEK_ADDON', $value, 'chaine', 0, '', $conf->entity); if ($result > 0) { @@ -313,7 +296,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Set the default PDF model while ensuring the model is enabled. -// FR: Définit le modèle PDF par défaut tout en s'assurant qu'il est activé. if ($action === 'setdoc' && !empty($value)) { $res = timesheetweekEnableDocumentModel($value, $docLabel, $scanDir); if ($res > 0) { @@ -327,7 +309,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Activate a PDF model without making it the default. -// FR: Active un modèle PDF sans le définir comme défaut. if ($action === 'setdocmodel' && !empty($value)) { $res = timesheetweekEnableDocumentModel($value, $docLabel, $scanDir); if ($res > 0) { @@ -338,7 +319,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Disable a PDF model and remove the default flag if needed. -// FR: Désactive un modèle PDF et supprime le statut par défaut si nécessaire. if ($action === 'delmodel' && !empty($value)) { $res = timesheetweekDisableDocumentModel($value); if ($res > 0) { @@ -352,7 +332,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Enable or disable the quarter-day selector for daily rate contracts. -// FR: Active ou désactive le sélecteur quart de jour pour les contrats au forfait jour. if ($action === 'setquarterday') { $targetValue = (int) GETPOST('value', 'int'); if ($targetValue !== 0) { @@ -417,7 +396,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Read the selected options so we can highlight them in the UI. -// FR: Lit les options sélectionnées pour les mettre en avant dans l'interface. $selectedNumbering = getDolGlobalString('TIMESHEETWEEK_ADDON', 'mod_timesheetweek_standard'); $defaultPdf = getDolGlobalString('TIMESHEETWEEK_ADDON_PDF', 'standard_timesheetweek'); $useQuarterDaySelector = getDolGlobalInt('TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', 0); @@ -428,11 +406,9 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a $directories = array_merge(array('/'), (array) $conf->modules_parts['models']); // EN: Prepare a lightweight object to test numbering module activation. -// FR: Prépare un objet léger pour tester l'activation des modules de numérotation. $sampleTimesheet = new TimesheetWeek($db); // EN: Fetch the enabled document models from the database. -// FR: Récupère les modèles de documents activés depuis la base. $enabledModels = array(); $sql = 'SELECT nom FROM '.MAIN_DB_PREFIX."document_model WHERE type='timesheetweek' AND entity IN (0, ".((int) $conf->entity).')'; $resql = $db->query($sql); @@ -444,7 +420,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Build the metadata arrays used by the HTML rendering below. -// FR: Construit les tableaux de métadonnées utilisés par l'affichage HTML ci-dessous. $numberingModules = timesheetweekListNumberingModules($directories, $langs, $sampleTimesheet, $selectedNumbering); $documentModels = timesheetweekListDocumentModels($directories, $langs, $enabledModels, $defaultPdf); $pageToken = function_exists('newToken') ? newToken() : ''; @@ -488,7 +463,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print '
'; // EN: Display the numbering modules with switch-based activation instead of radios. -// FR: Affiche les modules de numérotation avec des commutateurs plutôt qu'avec des radios. print '
'.$langs->trans('TimesheetWeekNumberingHelp').'
'; print '
'; @@ -524,7 +498,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print ''.(!empty($moduleInfo['example']) ? dol_escape_htmltag($moduleInfo['example']) : ' ').''; // EN: Render the activation toggle that selects the numbering model with CSRF protection. - // FR: Affiche le commutateur d’activation qui sélectionne le modèle de numérotation avec protection CSRF. print ''; if ($moduleInfo['active']) { print img_picto($langs->trans('Enabled'), 'switch_on'); @@ -546,7 +519,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print '
'; // EN: Display the helper switches dedicated to the daily-rate contract workflows. -// FR: Affiche les commutateurs dédiés aux workflows des contrats au forfait jour. print load_fiche_titre($langs->trans('TimesheetWeekDailyRateOptions'), '', 'setup'); print '
'.$langs->trans('TimesheetWeekDailyRateOptionsHelp').'
'; @@ -559,7 +531,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print ''; // EN: Render the switch dedicated to the quarter-day declaration helper. -// FR: Affiche le commutateur dédié à l'aide de déclaration des quarts de jour. print ''; print ''.$langs->trans('TimesheetWeekQuarterDayForDailyContract').''; print ''.$langs->trans('TimesheetWeekQuarterDayForDailyContractHelp').''; From ba045d8de58df76a3be23ee2be7dc28c1f7d0d56 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:31:35 +0100 Subject: [PATCH 7/8] Document weekly reminder feature --- ChangeLog.md | 4 ++++ README.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 8cbd24b..36eb6bf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,9 @@ # CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) +## 1.6.0 (20/06/2026) +- Ajoute un rappel hebdomadaire automatique par email configurable (activation, jour, heure, modèle) et une tâche planifiée dédiée avec bouton de test administrateur. + / Adds a configurable automatic weekly email reminder (enablement, day, time, template) plus a dedicated scheduled task and admin test button. + ## 1.5.0 (01/12/2025) - Ajoute une action de masse pour générer un PDF unique fusionnant les feuilles sélectionnées en utilisant le modèle par défaut. / Adds a mass action to generate a single merged PDF for selected timesheets using the default template. diff --git a/README.md b/README.md index 3273f32..364e614 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ TimesheetWeek ajoute une gestion hebdomadaire des feuilles de temps fidèle à l - Redirection automatique vers la feuille existante en cas de tentative de doublon afin d'éviter les saisies multiples. - Suivi des compteurs hebdomadaires de zones et de paniers directement sur les feuilles et recalcul automatique à chaque enregistrement. - Saisie dédiée pour les salariés en forfait jour grâce à des sélecteurs Journée/Matin/Après-midi convertissant automatiquement les heures. +- Rappel hebdomadaire automatique par email configurable (activation, jour, heure, modèle) avec tâche planifiée dédiée et bouton d'envoi de test administrateur. - Affichage des compteurs dans la liste hebdomadaire et ajout du libellé « Zone » sur chaque sélecteur quotidien pour clarifier la saisie. - Ligne de total en bas de la liste hebdomadaire pour additionner heures, zones, paniers et afficher la colonne de date de validation. - Création rapide d'une feuille d'heures via le raccourci « Ajouter » du menu supérieur. @@ -51,6 +52,7 @@ TimesheetWeek delivers weekly timesheet management that follows Dolibarr design - Automatic redirect to the existing timesheet when a duplicate creation is attempted. - Weekly counters for zones and meal allowances with automatic recomputation on each save. - Dedicated input for daily rate employees with Full day/Morning/Afternoon selectors that automatically convert hours. +- Configurable automatic weekly email reminder (enablement, weekday, time, template) with a dedicated scheduled task and admin test send button. - Counter display inside the weekly list plus a « Zone » caption on each daily selector for better input guidance. - Total row at the bottom of the weekly list to sum hours, zones, meals and expose the validation date column. - Quick creation shortcut available from the top-right « Add » menu. From b9d28e9202024f8ff73d086b07120e4a3d0cde29 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:51:00 +0100 Subject: [PATCH 8/8] Fix reminder mail include and clarify comments --- class/timesheetweek_reminder.class.php | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/class/timesheetweek_reminder.class.php b/class/timesheetweek_reminder.class.php index 1f15fe1..763dea0 100644 --- a/class/timesheetweek_reminder.class.php +++ b/class/timesheetweek_reminder.class.php @@ -1,5 +1,5 @@ +/* Copyright (C) 2025 Pierre Ardoin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -8,7 +8,7 @@ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License @@ -37,25 +37,27 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/functions.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; -require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.php'; +require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php'; require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; dol_include_once('/core/class/emailtemplates.class.php'); dol_include_once('/timesheetweek/class/timesheetweek.class.php'); /** - * Cron helper used to send weekly reminders. + * EN: Cron helper used to send weekly reminders. + * FR: Assistant cron utilisé pour envoyer les rappels hebdomadaires. */ class TimesheetweekReminder { /** - * Run cron job to send weekly reminder emails. + * EN: Run cron job to send weekly reminder emails. + * FR: Exécuter la tâche planifiée pour envoyer les rappels hebdomadaires par email. * - * @param DoliDB $db Database handler - * @param int $limit Optional limit for recipients - * @param int $forcerun Force execution (1) or use normal scheduling (0) - * @param array $targetUserIds Limit execution to specific user ids when provided - * @return int <0 if KO, >=0 if OK (number of emails sent) + * @param DoliDB $db Database handler + * @param int $limit Optional limit for recipients + * @param int $forcerun Force execution (1) or use normal scheduling (0) + * @param array $targetUserIds Limit execution to specific user ids when provided + * @return int <0 if KO, >=0 if OK (number of emails sent) */ public static function run($db, $limit = 0, $forcerun = 0, array $targetUserIds = array()) { @@ -238,12 +240,12 @@ public static function run($db, $limit = 0, $forcerun = 0, array $targetUserIds /** * Send a reminder test email to the current user using the configured template. * - * @param DoliDB $db Database handler - * @param User $user Current user - * @return int <0 if KO, >=0 if OK (number of emails sent) + * @param DoliDB $db Database handler + * @param User $user Current user + * @return int <0 if KO, >=0 if OK (number of emails sent) */ public static function sendTest($db, User $user) { return self::run($db, 1, 1, array((int) $user->id)); } -} +} \ No newline at end of file