Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
217 changes: 184 additions & 33 deletions admin/setup.php

Large diffs are not rendered by default.

251 changes: 251 additions & 0 deletions class/timesheetweek_reminder.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<?php
/* Copyright (C) 2025 Pierre Ardoin <[email protected]>
*
* 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 <https://www.gnu.org/licenses/>.
*/

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/functions.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.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');

/**
* EN: Cron helper used to send weekly reminders.
* FR: Assistant cron utilisé pour envoyer les rappels hebdomadaires.
*/
class TimesheetweekReminder
{
/**
* 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)
*/
public static function run($db, $limit = 0, $forcerun = 0, array $targetUserIds = array())
{
global $conf, $langs;

$langs->loadLangs(array('timesheetweek@timesheetweek'));

dol_syslog(__METHOD__, LOG_DEBUG);

$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;
}

$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;
}

$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;
}

$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 0;
}

$timezoneCode = !empty($conf->timezone) ? $conf->timezone : 'UTC';
$now = dol_now();
$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 ($currentWeekdayIso !== $reminderWeekday) {
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;
}
}

$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_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_WARNING);
return 0;
}

$from = getDolGlobalString('MAIN_MAIL_EMAIL_FROM', '');
if (empty($from)) {
$from = getDolGlobalString('MAIN_INFO_SOCIETE_MAIL', '');
}

$substitutions = getCommonSubstitutionArray($langs, 0, null, null, null);
complete_substitutions_array($substitutions, $langs, null);

$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)).')';
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);
}

$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;
}

$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__'] = $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);

$mail = new CMailFile($preparedSubject, $recipient, $from, $preparedBody, array(), array(), array(), '', '', 0, 0, '', '', '', 'utf-8');
$resultSend = $mail->sendfile();
if ($resultSend) {
$emailsSent++;
dol_syslog($langs->trans('TimesheetWeekReminderSendSuccess', $recipient), LOG_INFO);
} else {
dol_syslog($langs->trans('TimesheetWeekReminderSendFailed', $recipient), LOG_ERR);
$errors++;
}
}

$db->free($resql);

if ($errors > 0) {
return -$errors;
}

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));
}
}
28 changes: 14 additions & 14 deletions core/modules/modTimesheetWeek.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
20 changes: 20 additions & 0 deletions langs/en_US/timesheetweek.lang
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ 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.
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
Expand Down
20 changes: 20 additions & 0 deletions langs/fr_FR/timesheetweek.lang
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ 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.
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
Expand Down