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 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 '
';
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