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. diff --git a/admin/setup.php b/admin/setup.php index 1394de5..ca49120 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'; @@ -40,34 +39,48 @@ 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'; +require_once DOL_DOCUMENT_ROOT.'/core/class/cemailtemplate.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'); +$form = new Form($db); + +$sql = "SELECT rowid, label "; +$sql.= "FROM ".MAIN_DB_PREFIX."c_email_templates "; +$sql.= "WHERE active='1' "; +//$sql.= "AND enabled='1' "; +$sql.= "AND type_template = 'actioncomm_send' "; +//$sql.= "AND entity='".getEntity('timesheetweek')."' "; +//$sql.= "GROUP BY label"; +$result = $db->query($sql); +$templateOptions = array(); +if ($result) { + while ($obj = $db->fetch_object($result)) { + $templateOptions[(int) $obj->rowid] = $obj->label; + } +} + // 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; @@ -77,7 +90,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) { @@ -91,13 +103,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)."'"; } @@ -122,7 +132,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)) { @@ -134,7 +143,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; @@ -143,7 +151,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; @@ -210,7 +217,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; @@ -219,7 +225,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; @@ -275,17 +280,15 @@ 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 (in_array($action, array('setmodule', 'setdoc', 'setdocmodel', 'delmodel', 'setquarterday', 'savereminder', 'testreminder'), true)) { + if (function_exists('dol_verify_token')) { + if (empty($token) || dol_verify_token($token) <= 0) { + accessforbidden(); + } + } } // 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) { @@ -296,7 +299,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) { @@ -310,7 +312,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) { @@ -321,7 +322,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) { @@ -335,12 +335,11 @@ 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,19 +348,72 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } } +if ($action === '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'); + } + } +} + + if ($action === 'testreminder') { + $reminder = new TimesheetweekReminder($db); + $resultTest = $reminder->sendTest($db, $user); //$resultTest = TimesheetweekReminder::sendTest($db, $user); + //var_dump($resultTest); + if ($resultTest == 0) { + setEventMessages($langs->trans('TimesheetWeekReminderTestSuccess'), null, 'mesgs'); + } else { + setEventMessages($langs->trans('TimesheetWeekReminderTestError'), null, 'errors'); + } +} + // 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. -// 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); @@ -373,10 +425,29 @@ 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() : ''; +$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 = ''; @@ -397,7 +468,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 '
'; @@ -433,7 +503,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'); @@ -455,7 +524,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').'
'; @@ -468,7 +536,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').''; @@ -488,6 +555,68 @@ 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 ''; + +$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 ''; + +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 ''; +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..bcc8280 --- /dev/null +++ b/class/timesheetweek_reminder.class.php @@ -0,0 +1,313 @@ + + * + * 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/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('/timesheetweek/class/timesheetweek.class.php'); + + +/** + * Cron helper used to send weekly reminders. + */ +class TimesheetweekReminder extends CommonObject +{ + public $db; + public $error; + public $errors = array(); + public $output; + + public function __construct(DoliDB $db) + { + $this->db = $db; + } + /** + * 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) + * @param array $targetUserIds Limit execution to specific user ids when provided + * @return int <0 if KO, >=0 if OK (number of emails sent) + */ + public function run($dbInstance = null, $limit = 0, $forcerun = 0, array $targetUserIds = array()) //$dbInstance = null, + { + global $db, $conf, $user, $langs; + + /* + $db = $dbInstance; + if (empty($db) && !empty($GLOBALS['db'])) { + $db = $GLOBALS['db']; + } + */ + if (empty($db)) { + dol_syslog($langs->transnoentitiesnoconv('ErrorNoDatabase'), LOG_ERR); + return -1; + } + + $langs->loadLangs(array('timesheetweek@timesheetweek')); + + $forceExecution = !empty($forcerun); + if (!$forceExecution) { + $forceExecution = ((int) GETPOST('forcerun', 'int') > 0); + } + if (!$forceExecution) { + $action = GETPOST('action', 'aZ09'); + $confirm = GETPOST('confirm', 'alpha'); + if ($action === 'confirm_execute' && $confirm === 'yes') { + $forceExecution = true; + } + } + + $emailTemplateClassFile = ''; + if (is_readable(DOL_DOCUMENT_ROOT.'/core/class/cemailtemplate.class.php')) { + $emailTemplateClassFile = '/core/class/cemailtemplate.class.php'; + } elseif (is_readable(DOL_DOCUMENT_ROOT.'/core/class/emailtemplate.class.php')) { + $emailTemplateClassFile = '/core/class/emailtemplate.class.php'; + } + + if (!empty($emailTemplateClassFile)) { + dol_include_once($emailTemplateClassFile); + } + + if (!class_exists('CEmailTemplate') && !class_exists('EmailTemplate')) { + dol_syslog($langs->trans('ErrorFailedToLoadEmailTemplateClass'), LOG_ERR); + return -1; + } + + dol_syslog(__METHOD__, LOG_DEBUG); + + $reminderEnabled = getDolGlobalInt('TIMESHEETWEEK_REMINDER_ENABLED', 0, $conf->entity); + if (empty($reminderEnabled) && empty($forceExecution)) { + dol_syslog('TimesheetweekReminder: reminder disabled', LOG_INFO); + return 0; + } + + $reminderWeekday = getDolGlobalInt('TIMESHEETWEEK_REMINDER_WEEKDAY', 1, $conf->entity); + if ($reminderWeekday < 1 || $reminderWeekday > 7) { + dol_syslog($langs->trans('TimesheetWeekReminderWeekdayInvalid'), LOG_ERR); + return -1; + } + + $reminderHour = getDolGlobalString('TIMESHEETWEEK_REMINDER_HOUR', '18:00', $conf->entity); + 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, $conf->entity); + 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 = 5; + $lowerBound = max(0, $targetMinutes - $windowMinutes); + $upperBound = min(120, $targetMinutes + $windowMinutes); + + if (empty($forceExecution)) { + 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; + } + } + + $emailTemplate = null; + $templateFetch = 0; + if (class_exists('CEmailTemplate')) { + $emailTemplate = new CEmailTemplate($db); + if (method_exists($emailTemplate, 'apifetch')) { + $templateFetch = $emailTemplate->apifetch($templateId); + } else { + $templateFetch = $emailTemplate->fetch($templateId); + } + } elseif (class_exists('EmailTemplate')) { + $emailTemplate = new EmailTemplate($db); + $templateFetch = $emailTemplate->fetch($templateId); + } + + if (empty($emailTemplate)) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_ERR); + return -1; + } + + 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; + } + + $recipientUser = new User($db); + $fetchUser = $recipientUser->fetch($obj->rowid); + if ($fetchUser < 0) { + dol_syslog($recipientUser->error, LOG_ERR); + $errors++; + continue; + } + + $userSubstitutions = $substitutions; + $userSubstitutions['__USER_FIRSTNAME__'] = $recipientUser->firstname; + $userSubstitutions['__USER_LASTNAME__'] = $recipientUser->lastname; + $userSubstitutions['__USER_FULLNAME__'] = dolGetFirstLastname($recipientUser->firstname, $recipientUser->lastname); + $userSubstitutions['__USER_EMAIL__'] = $recipient; + complete_substitutions_array($userSubstitutions, $langs, null, $recipientUser); + + $preparedSubject = make_substitutions($subject, $userSubstitutions); + $preparedBody = make_substitutions($body, $userSubstitutions); + + $preparedSubject = dol_string_nohtmltag(html_entity_decode($preparedSubject, ENT_QUOTES, 'UTF-8')); + $preparedBodyHtml = html_entity_decode($preparedBody, ENT_QUOTES, 'UTF-8'); + $isHtmlBody = (!empty($preparedBodyHtml) && preg_match('/<[^>]+>/', $preparedBodyHtml)) ? 1 : 0; + $preparedBodyFinal = $isHtmlBody ? $preparedBodyHtml : dol_string_nohtmltag($preparedBodyHtml); + + $mail = new CMailFile($preparedSubject, $recipient, $from, $preparedBodyFinal, array(), array(), array(), '', '', 0, $isHtmlBody, '', '', '', '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; + + if ($errors) { + $this->error = $langs->trans('TimesheetWeekReminderSendFailed', $errors); + dol_syslog(__METHOD__." end - ".$this->error, LOG_ERR); + return 1; + }else{ + $this->output = $langs->trans('TimesheetWeekReminderSendSuccess', $emailsSent); + dol_syslog(__METHOD__." end - ".$this->output, LOG_INFO); + return 0; + } + } + + /** + * 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 function sendTest($db, User $user) + { + return self::run($db, 1, 1, array((int) $user->id)); + } +} diff --git a/core/modules/modTimesheetWeek.class.php b/core/modules/modTimesheetWeek.class.php index c11dd0c..a3a4900 100644 --- a/core/modules/modTimesheetWeek.class.php +++ b/core/modules/modTimesheetWeek.class.php @@ -112,7 +112,7 @@ public function __construct($db) } // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '1.5.0'; + $this->version = '1.6.0'; // Url to the file with your last numberversion of this module $this->url_last_version = 'https://moduleversion.lesmetiersdubatiment.fr/ver.php?m=timesheetweek'; @@ -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' => 5, + 'unitfrequency' => 60, + 'status' => 1, + 'test' => 'isModEnabled("timesheetweek")', + 'priority' => 50, + ), ); /* END MODULEBUILDER CRON */ // Example: $this->cronjobs=array( @@ -777,37 +777,71 @@ public function init($options = '') $extrafields->fetch_name_optionals_label('user'); $dailyRateVisibility = '-1'; $dailyRateSharedEntity = '0'; - if (empty($extrafields->attributes['user']['label']['lmdb_daily_rate'])) { - // EN: Register the daily rate toggle on employees when the module is activated. - // FR: Enregistre l'option de forfait jour sur les salariés lors de l'activation du module. - $extrafields->addExtraField('lmdb_daily_rate', 'TimesheetWeekDailyRateLabel', 'boolean', 100, '', 'user', 0, 0, '', '', 0, '', $dailyRateVisibility, '', '', $dailyRateSharedEntity, 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")', 0, 0); - } else { - // EN: Align the existing daily rate toggle with the latest visibility and sharing rules. - // FR: Aligne l'option de forfait jour existante avec les nouvelles règles de visibilité et de partage. - $extrafields->updateExtraField('lmdb_daily_rate', 'TimesheetWeekDailyRateLabel', 'boolean', 100, '', 'user', 0, 0, '', '', 0, '', $dailyRateVisibility, '', '', $dailyRateSharedEntity, 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")', 0, 0); - } + if (empty($extrafields->attributes['user']['label']['lmdb_daily_rate'])) { + // EN: Register the daily rate toggle on employees when the module is activated. + // FR: Enregistre l'option de forfait jour sur les salariés lors de l'activation du module. + $extrafields->addExtraField('lmdb_daily_rate', 'TimesheetWeekDailyRateLabel', 'boolean', 100, '', 'user', 0, 0, '', '', 0, '', $dailyRateVisibility, '', '', $dailyRateSharedEntity, 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")', 0, 0); + } else { + // EN: Avoid reapplying updates on the daily rate toggle to prevent redundant schema operations. + // FR: Éviter de réappliquer les mises à jour du forfait jour pour prévenir les opérations de schéma redondantes. + $currentVisibility = (string) ($extrafields->attributes['user']['visible']['lmdb_daily_rate'] ?? ''); + $currentSharedEntity = (string) ($extrafields->attributes['user']['shared']['lmdb_daily_rate'] ?? ''); + $currentType = (string) ($extrafields->attributes['user']['type']['lmdb_daily_rate'] ?? ''); + $requiresUpdate = ($currentType !== 'boolean' || $currentVisibility !== $dailyRateVisibility || $currentSharedEntity !== $dailyRateSharedEntity); + if ($requiresUpdate) { + // EN: Refresh the existing daily rate toggle only when its definition diverges from the expected one. + // FR: Rafraîchir l'option de forfait jour existante uniquement lorsque sa définition diverge de celle attendue. + $resultUpdateDailyRate = $extrafields->updateExtraField('lmdb_daily_rate', 'TimesheetWeekDailyRateLabel', 'boolean', 100, '', 'user', 0, 0, '', '', 0, '', $dailyRateVisibility, '', '', $dailyRateSharedEntity, 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")', 0, 0); + if ($resultUpdateDailyRate < 0) { + $this->error = $extrafields->error; + return -1; + } + } + } - // EN: Ensure existing installations receive the daily_rate column for time entries. - // FR: Garantit que les installations existantes reçoivent la colonne daily_rate pour les lignes de temps. - $sqlCheckDailyRate = "SHOW COLUMNS FROM ".$this->db->prefix()."timesheet_week_line LIKE 'daily_rate'"; - $resqlCheckDailyRate = $this->db->query($sqlCheckDailyRate); - if (!$resqlCheckDailyRate) { - // EN: Abort activation when the structure verification query fails. - // FR: Interrompt l'activation si la requête de vérification de structure échoue. - $this->error = $this->db->lasterror(); - return -1; - } - $hasDailyRateColumn = (bool) $this->db->num_rows($resqlCheckDailyRate); - $this->db->free($resqlCheckDailyRate); - if (!$hasDailyRateColumn) { - $sqlAdd = "ALTER TABLE ".MAIN_DB_PREFIX."timesheet_week_line ADD COLUMN daily_rate INT NOT NULL DEFAULT 0"; - if (!$this->db->query($sqlAdd)) { - // EN: Stop activation when adding the column fails to keep database consistent. - // FR: Stoppe l'activation si l'ajout de la colonne échoue pour conserver une base cohérente. + // EN: Ensure existing installations receive the daily_rate column for time entries. + // FR: Garantit que les installations existantes reçoivent la colonne daily_rate pour les lignes de temps. + $sqlCheckDailyRate = "SHOW COLUMNS FROM ".$this->db->prefix()."timesheet_week_line LIKE 'daily_rate'"; + $resqlCheckDailyRate = $this->db->query($sqlCheckDailyRate); + if (!$resqlCheckDailyRate) { + // EN: Abort activation when the structure verification query fails. + // FR: Interrompt l'activation si la requête de vérification de structure échoue. $this->error = $this->db->lasterror(); return -1; } - } + $hasDailyRateColumn = (bool) $this->db->num_rows($resqlCheckDailyRate); + $this->db->free($resqlCheckDailyRate); + if (!$hasDailyRateColumn) { + $sqlAdd = "ALTER TABLE ".MAIN_DB_PREFIX."timesheet_week_line ADD COLUMN daily_rate INT NOT NULL DEFAULT 0"; + if (!$this->db->query($sqlAdd)) { + // EN: Stop activation when adding the column fails to keep database consistent. + // FR: Stoppe l'activation si l'ajout de la colonne échoue pour conserver une base cohérente. + $this->error = $this->db->lasterror(); + return -1; + } + } + + // EN: Ensure existing installations keep the PDF model column on weekly sheets. + // FR: Garantit que les installations existantes conservent la colonne du modèle PDF sur les feuilles hebdomadaires. + $sqlCheckModelPdf = "SHOW COLUMNS FROM ".$this->db->prefix()."timesheet_week LIKE 'model_pdf'"; + $resqlCheckModelPdf = $this->db->query($sqlCheckModelPdf); + if (!$resqlCheckModelPdf) { + // EN: Abort activation when the PDF model check fails. + // FR: Interrompre l'activation si la vérification du modèle PDF échoue. + $this->error = $this->db->lasterror(); + return -1; + } + $hasModelPdfColumn = (bool) $this->db->num_rows($resqlCheckModelPdf); + $this->db->free($resqlCheckModelPdf); + if (!$hasModelPdfColumn) { + $sqlAddModelPdf = "ALTER TABLE ".MAIN_DB_PREFIX."timesheet_week ADD COLUMN model_pdf VARCHAR(255) DEFAULT NULL AFTER status"; + if (!$this->db->query($sqlAddModelPdf)) { + // EN: Stop activation if adding the PDF model column fails to preserve schema consistency. + // FR: Stopper l'activation si l'ajout de la colonne de modèle PDF échoue pour préserver la cohérence du schéma. + $this->error = $this->db->lasterror(); + return -1; + } + } // Permissions $this->remove($options); @@ -845,83 +879,106 @@ public function init($options = '') dolibarr_set_const($this->db, 'TIMESHEETWEEK_ADDON_PDF', 'standard_timesheetweek', 'chaine', 0, '', $conf->entity); } - // EN: Register Multicompany sharing metadata when the module is enabled. - // FR: Enregistrer les métadonnées de partage Multicompany lors de l'activation du module. - dol_include_once('/timesheetweek/class/actions_timesheetweek.class.php'); - - // EN: Start from the current external module sharing configuration or an empty set. - // FR: Partir de la configuration de partage des modules externes actuelle ou d'un ensemble vide. - $externalmodule = json_decode((string) ($conf->global->MULTICOMPANY_EXTERNAL_MODULES_SHARING ?? ''), true); - if (!is_array($externalmodule)) { - // EN: Guarantee an array structure even when the constant is missing or invalid. - // FR: Garantir une structure de tableau même lorsque la constante est absente ou invalide. - $externalmodule = array(); - } + // Register Multicompany sharing metadata when the module is enabled. + dol_include_once('/timesheetweek/class/actions_timesheetweek.class.php'); - // EN: Initialize the parameters container before filling it with sharing data. - // FR: Initialiser le conteneur de paramètres avant de le remplir avec les données de partage. - $params = array(); - if (class_exists('ActionsTimesheetweek')) { - // EN: Reuse the hook helper to expose the sharing parameters to Multicompany. - // FR: Réutiliser l'assistant de hook pour exposer les paramètres de partage à Multicompany. - $params = ActionsTimesheetweek::getMulticompanySharingDefinition(); - } + // Start from the current external module sharing configuration or an empty set. + $externalmodule = json_decode((string) ($conf->global->MULTICOMPANY_EXTERNAL_MODULES_SHARING ?? ''), true); + if (!is_array($externalmodule)) { + // Guarantee an array structure even when the constant is missing or invalid. + $externalmodule = array(); + } + + // Initialize the parameters container before filling it with sharing data. + $params = array(); + if (class_exists('ActionsTimesheetweek')) { + // Reuse the hook helper to expose the sharing parameters to Multicompany. + $params = ActionsTimesheetweek::getMulticompanySharingDefinition(); + } - if (!empty($params)) { - // EN: Merge TimesheetWeek definitions with any existing external module configuration. - // FR: Fusionner les définitions TimesheetWeek avec toute configuration de module externe existante. - $externalmodule = array_merge($externalmodule, $params); + if (!empty($params)) { + // Merge TimesheetWeek definitions with any existing external module configuration. + $externalmodule = array_merge($externalmodule, $params); - // EN: Persist the refreshed configuration in the Dolibarr constants table. - // FR: Persister la configuration actualisée dans la table des constantes de Dolibarr. - $jsonformat = json_encode($externalmodule); - dolibarr_set_const($this->db, 'MULTICOMPANY_EXTERNAL_MODULES_SHARING', $jsonformat, 'chaine', 0, '', $conf->entity); - } + // Persist the refreshed configuration in the Dolibarr constants table. + $jsonformat = json_encode($externalmodule); + dolibarr_set_const($this->db, 'MULTICOMPANY_EXTERNAL_MODULES_SHARING', $jsonformat, 'chaine', 0, '', $conf->entity); + } - return $this->_init($sql, $options); + $resultInit = $this->_init($sql, $options); + if ($resultInit <= 0) { + return $resultInit; + } + + $cronStatus = $this->setReminderCronStatus(1); + if ($cronStatus < 0) { + return $cronStatus; + } + + return $resultInit; } /** - * Function called when module is disabled. - * Remove from database constants, boxes and permissions from Dolibarr database. - * Data directories are not deleted + * Function called when module is disabled. + * Remove from database constants, boxes and permissions from Dolibarr database. + * Data directories are not deleted * - * @param string $options Options when enabling module ('', 'noboxes') - * @return int<-1,1> 1 if OK, <=0 if KO + * @param string $options Options when enabling module ('', 'noboxes') + * @return int<-1,1> 1 if OK, <=0 if KO */ public function remove($options = '') { - // EN: Access the global configuration to manipulate stored constants. - // FR: Accéder à la configuration globale pour manipuler les constantes stockées. - global $conf; - - // EN: Load the hook helper to clean the sharing definition on deactivation. - // FR: Charger l'assistant de hook pour nettoyer la définition de partage à la désactivation. - dol_include_once('/timesheetweek/class/actions_timesheetweek.class.php'); - - // EN: Decode the current external module sharing configuration safely. - // FR: Décoder prudemment la configuration de partage des modules externes actuelle. - $externalmodule = json_decode((string) ($conf->global->MULTICOMPANY_EXTERNAL_MODULES_SHARING ?? ''), true); - if (!is_array($externalmodule)) { - // EN: Fallback to an empty array when the stored value is not valid JSON. - // FR: Revenir à un tableau vide lorsque la valeur stockée n'est pas un JSON valide. - $externalmodule = array(); - } + // Access the global configuration to manipulate stored constants. + global $conf; + + // Load the hook helper to clean the sharing definition on deactivation. + dol_include_once('/timesheetweek/class/actions_timesheetweek.class.php'); - // EN: Determine the key used to store TimesheetWeek sharing data. - // FR: Déterminer la clé utilisée pour stocker les données de partage TimesheetWeek. - $sharingKey = class_exists('ActionsTimesheetweek') ? ActionsTimesheetweek::MULTICOMPANY_SHARING_ROOT_KEY : 'timesheetweek'; + // Decode the current external module sharing configuration safely. + $externalmodule = json_decode((string) ($conf->global->MULTICOMPANY_EXTERNAL_MODULES_SHARING ?? ''), true); + if (!is_array($externalmodule)) { + // Fallback to an empty array when the stored value is not valid JSON. + $externalmodule = array(); + } - // EN: Remove the module definition so Multicompany forgets our sharing options. - // FR: Retirer la définition du module pour que Multicompany oublie nos options de partage. - unset($externalmodule[$sharingKey]); + // Determine the key used to store TimesheetWeek sharing data. + $sharingKey = class_exists('ActionsTimesheetweek') ? ActionsTimesheetweek::MULTICOMPANY_SHARING_ROOT_KEY : 'timesheetweek'; - // EN: Persist the cleaned configuration back into Dolibarr constants. - // FR: Persister la configuration nettoyée dans les constantes Dolibarr. - $jsonformat = json_encode($externalmodule); - dolibarr_set_const($this->db, 'MULTICOMPANY_EXTERNAL_MODULES_SHARING', $jsonformat, 'chaine', 0, '', $conf->entity); + // Remove the module definition so Multicompany forgets our sharing options. + unset($externalmodule[$sharingKey]); - $sql = array(); - return $this->_remove($sql, $options); - } + // Persist the cleaned configuration back into Dolibarr constants. + $jsonformat = json_encode($externalmodule); + dolibarr_set_const($this->db, 'MULTICOMPANY_EXTERNAL_MODULES_SHARING', $jsonformat, 'chaine', 0, '', $conf->entity); + + $this->setReminderCronStatus(0); + + $sql = array(); + return $this->_remove($sql, $options); + } + + /** + * Update the cron status for the reminder job. + * + * @param int $status Target status (1 enabled, 0 disabled) + * @return int 1 if OK, <0 if error + */ + protected function setReminderCronStatus($status) + { + if (empty($this->db)) { + return -1; + } + + $statusValue = ((int) $status === 1) ? 1 : 0; + $sql = 'UPDATE '.MAIN_DB_PREFIX."cronjob SET status = ".$statusValue; + $sql .= " WHERE jobtype = 'method' AND classesname = '/timesheetweek/class/timesheetweek_reminder.class.php' AND methodename = 'run'"; + + $resql = $this->db->query($sql); + if (!$resql) { + $this->error = $this->db->lasterror(); + return -1; + } + + return 1; + } } diff --git a/core/modules/timesheetweek/mod_timesheetweek_standard.php b/core/modules/timesheetweek/mod_timesheetweek_standard.php index fe79b79..04e9bfb 100644 --- a/core/modules/timesheetweek/mod_timesheetweek_standard.php +++ b/core/modules/timesheetweek/mod_timesheetweek_standard.php @@ -95,11 +95,6 @@ public function canBeActivated($object) $sql = "SELECT MAX(CAST(SUBSTRING(ref FROM ".$posindice.") AS SIGNED)) as max"; $sql .= " FROM ".$db->prefix()."timesheetweek_timesheetweek"; $sql .= " WHERE ref LIKE '".$db->escape($this->prefix)."____-%'"; - if ($object->ismultientitymanaged == 1) { - $sql .= " AND entity = ".$conf->entity; - } elseif (!is_numeric($object->ismultientitymanaged)) { // @phan-suppress-current-line PhanPluginEmptyStatementIf - // TODO - } $resql = $db->query($sql); if ($resql) { @@ -133,11 +128,6 @@ public function getNextValue($object) $sql = "SELECT MAX(CAST(SUBSTRING(ref FROM ".$posindice.") AS SIGNED)) as max"; $sql .= " FROM ".$db->prefix()."timesheetweek_timesheetweek"; $sql .= " WHERE ref LIKE '".$db->escape($this->prefix)."____-%'"; - if ($object->ismultientitymanaged == 1) { - $sql .= " AND entity = ".$conf->entity; - } elseif (!is_numeric($object->ismultientitymanaged)) { - // TODO - } $resql = $db->query($sql); if ($resql) { diff --git a/langs/en_US/timesheetweek.lang b/langs/en_US/timesheetweek.lang index abe3a41..7c0ea4b 100644 --- a/langs/en_US/timesheetweek.lang +++ b/langs/en_US/timesheetweek.lang @@ -27,6 +27,29 @@ 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 +TimesheetWeekReminderTestError = Unable to send the test reminder email. +TimesheetWeekReminderTemplateLabel = Timesheetweek - Timesheet reminder +TimesheetWeekReminderTemplateSubject = Timesheet submission reminder +TimesheetWeekReminderTemplateBody = Hello __TSW_USER_FIRSTNAME__,\nPlease submit your weekly timesheet before Monday 8:00.\n__TSW_TIMESHEET_NEW_URL__\nRegards, __TSW_DOLIBARR_TITLE__ 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..d5aa9f3 100644 --- a/langs/fr_FR/timesheetweek.lang +++ b/langs/fr_FR/timesheetweek.lang @@ -27,6 +27,29 @@ 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é +TimesheetWeekReminderTestError = Échec de l'envoi du mail de test. +TimesheetWeekReminderTemplateLabel = Timesheetweek - Rappel des feuilles d'heures +TimesheetWeekReminderTemplateSubject = Rappel d'envoi des feuilles d'heures +TimesheetWeekReminderTemplateBody = Bonjour __TSW_USER_FIRSTNAME__,\nMerci de soumettre votre feuille d'heures de la semaine pour lundi 8h.\n__TSW_TIMESHEET_NEW_URL__\nBon weekend, __TSW_DOLIBARR_TITLE__ NewSection=Nouvelle section TIMESHEETWEEK_MYPARAM1 = Mon paramètre 1 TIMESHEETWEEK_MYPARAM1Tooltip = Info-bulle de mon paramètre 1 diff --git a/sql/data.sql b/sql/data.sql new file mode 100644 index 0000000..3df3103 --- /dev/null +++ b/sql/data.sql @@ -0,0 +1,11 @@ +INSERT INTO llx_c_email_templates (entity,module,type_template,lang,private,fk_user,datec,label,position,active,enabled,joinfiles,topic,content) + VALUES ( + 1,'timesheetweek','actioncomm_send','fr_FR', 0,NULL, NOW(), + 'Rappel du vendredi soir', + 100, + 1, + 'isModEnabled(\"timesheetweek\")', + NULL, + "Rappel Feuilles d\'heures hebodmadaires", + "Bonjour,

Merci de soumettre vos feuilles d\'heures de la semaine pour lundi matin 8h.

Bon week-end.
" + ); diff --git a/sql/llx_timesheet_week.sql b/sql/llx_timesheet_week.sql index 9e47caf..48fedc9 100644 --- a/sql/llx_timesheet_week.sql +++ b/sql/llx_timesheet_week.sql @@ -40,6 +40,60 @@ CREATE TABLE IF NOT EXISTS llx_timesheet_week ( FOREIGN KEY (fk_user_valid) REFERENCES llx_user (rowid) ) ENGINE=innodb; + +-- EN: Insert default weekly reminder email template when full email columns exist with code +INSERT INTO llx_c_email_templates ( +entity, +private, +module, +type_template, +label, +lang, +position, +active, +enabled, +joinfiles, +email_from, +email_to, +email_tocc, +email_tobcc, +topic, +content +) +SELECT +0, +0, +'timesheetweek', +'timesheetweek', +'TIMESHEETWEEK_REMINDER', +'fr_FR', +0, +1, +1, +0, +'', +'', +'', +'', +'Rappel d''envoi des feuilles d''heures', +'Bonjour __TSW_USER_FIRSTNAME__,\\nMerci de soumettre votre feuille d''heures de la semaine pour lundi 8h.\\n__TSW_TIMESHEET_NEW_URL__\\nBon weekend, __TSW_DOLIBARR_TITLE__' +WHERE EXISTS ( +SELECT 1 +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() +AND TABLE_NAME = 'llx_c_email_templates' +AND COLUMN_NAME IN ('email_tocc', 'email_tobcc', 'email_from', 'email_to', 'joinfiles') +GROUP BY TABLE_NAME +HAVING COUNT(DISTINCT COLUMN_NAME) = 6 +) +AND NOT EXISTS ( +SELECT 1 +FROM llx_c_email_templates +WHERE module = 'timesheetweek' +AND entity IN (0, 1) +AND label = 'TIMESHEETWEEK_REMINDER' +); + -- TimesheetWeek - lines CREATE TABLE IF NOT EXISTS llx_timesheet_week_line ( rowid INT AUTO_INCREMENT PRIMARY KEY, diff --git a/sql/update_all.sql b/sql/update_all.sql index 102cde8..bf0d64e 100644 --- a/sql/update_all.sql +++ b/sql/update_all.sql @@ -1,2 +1,16 @@ --- EN: Add the PDF model column to existing tables / FR: Ajoute la colonne de modèle PDF aux tables existantes. -ALTER TABLE llx_timesheet_week ADD COLUMN IF NOT EXISTS model_pdf VARCHAR(255) DEFAULT NULL AFTER status; +-- EN: Add the PDF model column to existing tables when missing. +SET @tsw_has_model_pdf := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'llx_timesheet_week' + AND COLUMN_NAME = 'model_pdf' +); +SET @tsw_add_model_pdf := IF( + @tsw_has_model_pdf = 0, + 'ALTER TABLE llx_timesheet_week ADD COLUMN model_pdf VARCHAR(255) DEFAULT NULL AFTER status', + 'SELECT 1' +); +PREPARE tsw_stmt FROM @tsw_add_model_pdf; +EXECUTE tsw_stmt; +DEALLOCATE PREPARE tsw_stmt;