diff --git a/ChangeLog.md b/ChangeLog.md index 9aeb3c7..12f9af5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,18 @@ # CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) +## 1.1.1 +- Mise à "plat" des permissions pour régler un problème d'affichage des PDF. / "Flattening" permissions to fix a PDF display issue. + +## 1.1.0 +- Ajoute la massaction « Générer le PDF de synthèse » afin de produire un récapitulatif multi-salariés dans un PDF conforme aux standards Dolibarr. / Adds the "Generate summary PDF" mass action to produce a multi-employee PDF recap compliant with Dolibarr standards. + +## 1.0.7 +- Harmonise les pictogrammes d'approbation et de refus avec les icônes Dolibarr natives. / Harmonises the approval and refusal pictograms with native Dolibarr icons. +- Ajoute la massaction « Sceller » et applique les contrôles de permissions Dolibarr aux actions de masse. / Adds the "Seal" mass action and applies Dolibarr permission checks to bulk operations. +- Limite la suppression massive aux feuilles en brouillon et avertit en cas de sélection invalide. / Restricts bulk deletion to draft sheets and warns when invalid selections are detected. +- Complète les traductions de la liste pour les actions de masse. / Completes list translations for bulk actions. + + ## 1.0.6 - Réorganise le menu gauche pour afficher « Nouvelle feuille » avant « Liste ». / Reorders the left menu to display "New sheet" before "List". - Ajoute les entrées « TimesheetWeek » dans les menus principaux Agenda et Projet. / Adds the "TimesheetWeek" entries under the Agenda and Project main menus. @@ -31,6 +44,10 @@ - Corrige un problème dans les options de partage du module "Multicompany". / Fixes an issue in the "Multicompany" module sharing options. - Repositionne les totaux correctement. / Correctly repositions totals. - Inversion de position de "Nouvelle Feuille d'heure" et "Liste" dans le menu gauche. / Swaps the position of "New Timesheet" and "List" in the left menu. +- Le tableau du PDF de synthèse occupe désormais toute la largeur imprimable pour une meilleure lisibilité. / Summary PDF table now spans the full printable width for improved readability. +- Ajoute les entêtes et pieds de page standards à chaque page du PDF de synthèse et garantit que le premier tableau débute dès la première page. / Adds standard headers and footers to every summary PDF page and ensures the first table starts on the opening page. +- Remonte le pied de page du PDF de synthèse pour réduire la marge basse et améliorer la lisibilité. / Raises the summary PDF footer to reduce the bottom margin and improve readability. +- Garantit que le contenu du PDF de synthèse reste entre l'entête et le pied de page sur chaque feuille. / Ensures the summary PDF content stays between the header and footer on every sheet. - Corrige un problème pouvant masquer les tâches clôturées dans les Fiches passées. / Fix an issue that could hide closed tasks in passed timesheet. ## 1.0.1 diff --git a/ajax/timesheetweek.php b/ajax/timesheetweek.php index 0f2e967..ed2d60f 100644 --- a/ajax/timesheetweek.php +++ b/ajax/timesheetweek.php @@ -76,9 +76,9 @@ // EN: Evaluate write permissions, including subordinate scope, before processing the request. // FR: Évalue les permissions d'écriture, y compris sur les subordonnés, avant de traiter la requête. -$permWrite = $user->hasRight('timesheetweek', 'timesheetweek', 'write'); -$permWriteChild = $user->hasRight('timesheetweek', 'timesheetweek', 'writeChild'); -$permWriteAll = $user->hasRight('timesheetweek', 'timesheetweek', 'writeAll'); +$permWrite = $user->hasRight('timesheetweek', 'write'); +$permWriteChild = $user->hasRight('timesheetweek', 'writeChild'); +$permWriteAll = $user->hasRight('timesheetweek', 'writeAll'); $canWriteAll = (!empty($user->admin) || $permWriteAll); if (!($permWrite || $permWriteChild || $canWriteAll)) { // EN: Return a JSON 403 response when the user cannot edit any timesheet. diff --git a/class/actions_timesheetweek.class.php b/class/actions_timesheetweek.class.php index 6f046bc..1590599 100644 --- a/class/actions_timesheetweek.class.php +++ b/class/actions_timesheetweek.class.php @@ -69,7 +69,7 @@ public function menuDropdownQuickaddItems($parameters, &$object, &$action, $hook // EN: Evaluate user permissions to control quick creation visibility. // FR: Évaluer les droits de l'utilisateur pour contrôler la visibilité de la création rapide. - $hasWriteRight = $user->hasRight('timesheetweek', 'timesheetweek', 'write') || $user->hasRight('timesheetweek', 'timesheetweek', 'writeChild') || $user->hasRight('timesheetweek', 'timesheetweek', 'writeAll'); + $hasWriteRight = $user->hasRight('timesheetweek', 'write') || $user->hasRight('timesheetweek', 'writeChild') || $user->hasRight('timesheetweek', 'writeAll'); // EN: Inject the quick creation entry with translated metadata. // FR: Injecter l'entrée de création rapide avec des métadonnées traduites. diff --git a/core/modules/modTimesheetWeek.class.php b/core/modules/modTimesheetWeek.class.php index 0681696..982faa2 100644 --- a/core/modules/modTimesheetWeek.class.php +++ b/core/modules/modTimesheetWeek.class.php @@ -80,8 +80,8 @@ public function __construct($db) $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@timesheetweek' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '1.0.6'; // EN: Reorders the left menu, adds agenda and project menu entries and fixes the list page limit selector. - // FR: Réorganise le menu gauche, ajoute les entrées de menus agenda et projet et corrige le sélecteur de limite dans la liste. + $this->version = '1.1.1'; // EN: "Flattening" permissions to fix a PDF display issue. + // FR: Mise à "plat" des permissions pour régler un problème d'affichage des PDF. // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -305,96 +305,110 @@ public function __construct($db) /* BEGIN MODULEBUILDER PERMISSIONS */ $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 0 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightReadOwn'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'read'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'read'; + $this->rights[$r][5] = ''; $r++; $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 1 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightReadChild'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'readChild'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'readChild'; + $this->rights[$r][5] = ''; $r++; $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 2 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightReadAll'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'readAll'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'readAll'; + $this->rights[$r][5] = ''; $r++; $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 3 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightWriteOwn'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'write'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'write'; + $this->rights[$r][5] = ''; $r++; $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 4 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightWriteChild'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'writeChild'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'writeChild'; + $this->rights[$r][5] = ''; $r++; $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 5 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightWriteAll'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'writeAll'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'writeAll'; + $this->rights[$r][5] = ''; $r++; $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 6 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightDeleteOwn'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'delete'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'delete'; + $this->rights[$r][5] = ''; $r++; $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 7 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightDeleteChild'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'deleteChild'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'deleteChild'; + $this->rights[$r][5] = ''; $r++; $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 8 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightDeleteAll'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'deleteAll'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'deleteAll'; + $this->rights[$r][5] = ''; $r++; // EN: Legacy validation permission kept for backward compatibility // FR : Droit de validation générique conservé pour compatibilité ascendante $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 9 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightValidateGeneric'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'validate'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'validate'; + $this->rights[$r][5] = ''; $r++; // EN: Allow managers to validate their own timesheets only // FR : Autorise un utilisateur à valider uniquement ses propres feuilles $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 10 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightValidateOwn'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'validateOwn'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'validateOwn'; + $this->rights[$r][5] = ''; $r++; // EN: Allow validation on subordinate timesheets // FR : Autorise la validation des feuilles des subordonnés $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 11 + 1); $this->rights[$r][1] = $langs->trans('TimesheetWeekRightValidateChild'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'validateChild'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'validateChild'; + $this->rights[$r][5] = ''; + $r++; + // EN: Allow global validation on all employee timesheets + // FR : Autorise la validation de toutes les feuilles de temps + $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 12 + 1); + $this->rights[$r][1] = $langs->trans('TimesheetWeekRightValidateAll'); + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'validateAll'; + $this->rights[$r][5] = ''; + $r++; + + // EN: Allow sealing approved timesheets. + // FR : Autorise le scellement des feuilles approuvées. + $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 13 + 1); + $this->rights[$r][1] = $langs->trans('TimesheetWeekRightSeal'); + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'seal'; + $this->rights[$r][5] = ''; + $r++; + + // EN: Allow reopening sealed timesheets. + // FR : Autorise le descellage des feuilles scellées. + $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 14 + 1); + $this->rights[$r][1] = $langs->trans('TimesheetWeekRightUnseal'); + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'unseal'; + $this->rights[$r][5] = ''; $r++; - // EN: Allow global validation on all employee timesheets - // FR : Autorise la validation de toutes les feuilles de temps - $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 12 + 1); - $this->rights[$r][1] = $langs->trans('TimesheetWeekRightValidateAll'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'validateAll'; - $r++; - - // EN: Allow sealing approved timesheets. - // FR : Autorise le scellement des feuilles approuvées. - $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 13 + 1); - $this->rights[$r][1] = $langs->trans('TimesheetWeekRightSeal'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'seal'; - $r++; - - // EN: Allow reopening sealed timesheets. - // FR : Autorise le descellage des feuilles scellées. - $this->rights[$r][0] = $this->numero . sprintf('%02d', (0 * 10) + 14 + 1); - $this->rights[$r][1] = $langs->trans('TimesheetWeekRightUnseal'); - $this->rights[$r][4] = 'timesheetweek'; - $this->rights[$r][5] = 'unseal'; - $r++; - - /* END MODULEBUILDER PERMISSIONS */ + /* END MODULEBUILDER PERMISSIONS */ // Main menu entries to add $this->menu = array(); @@ -413,7 +427,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', // Define condition to show or hide menu entry. Use 'isModEnabled("timesheetweek")' if entry must be visible if module is enabled. - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")', // Use 'perms'=>'$user->hasRight("timesheetweek", "timesheetweek", "read")' if you want your menu with a permission rules + 'perms' => '$user->hasRight("timesheetweek", "read")', // Use 'perms'=>'$user->hasRight("timesheetweek", "read")' if you want your menu with a permission rules 'target' => '', 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both ); @@ -452,7 +466,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")', + 'perms' => '$user->hasRight("timesheetweek", "read")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -467,7 +481,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "write")', + 'perms' => '$user->hasRight("timesheetweek", "write")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -482,7 +496,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")', + 'perms' => '$user->hasRight("timesheetweek", "read")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -500,7 +514,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")', + 'perms' => '$user->hasRight("timesheetweek", "read")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -515,7 +529,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "write")', + 'perms' => '$user->hasRight("timesheetweek", "write")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -530,7 +544,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")', + 'perms' => '$user->hasRight("timesheetweek", "read")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -549,7 +563,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")', + 'perms' => '$user->hasRight("timesheetweek", "read")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -564,7 +578,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "write")', + 'perms' => '$user->hasRight("timesheetweek", "write")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -579,7 +593,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")', + 'perms' => '$user->hasRight("timesheetweek", "read")', 'target' => '', 'user' => 2, 'object' => 'TimesheetWeek' @@ -599,7 +613,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', // Define condition to show or hide menu entry. Use 'isModEnabled("timesheetweek")' if entry must be visible if module is enabled. - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")', + 'perms' => '$user->hasRight("timesheetweek", "read")', 'target' => '', 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both 'object' => 'TimesheetWeek' @@ -614,7 +628,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', // Define condition to show or hide menu entry. Use 'isModEnabled("timesheetweek")' if entry must be visible if module is enabled. Use '$leftmenu==\'system\'' to show if leftmenu system is selected. - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "write")' + 'perms' => '$user->hasRight("timesheetweek", "write")' 'target' => '', 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both 'object' => 'TimesheetWeek' @@ -629,7 +643,7 @@ public function __construct($db) 'langs' => 'timesheetweek@timesheetweek', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. 'position' => 1000 + $r, 'enabled' => 'isModEnabled("timesheetweek")', // Define condition to show or hide menu entry. Use 'isModEnabled("timesheetweek")' if entry must be visible if module is enabled. - 'perms' => '$user->hasRight("timesheetweek", "timesheetweek", "read")' + 'perms' => '$user->hasRight("timesheetweek", "read")' 'target' => '', 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both 'object' => 'TimesheetWeek' diff --git a/langs/en_US/timesheetweek.lang b/langs/en_US/timesheetweek.lang index 09bfcaa..45c9bb4 100644 --- a/langs/en_US/timesheetweek.lang +++ b/langs/en_US/timesheetweek.lang @@ -112,6 +112,43 @@ SealTimesheet = Seal UnsealTimesheet = Unseal ConfirmValidate = Do you confirm the approval of this timesheet? ConfirmRefuse = Do you confirm the refusal of this timesheet? +ApproveSelection = Approve selection +RefuseSelection = Refuse selection +SealSelection = Seal selection +DeleteSelection = Delete selection +TimesheetWeekMassApproveSuccess = %s timesheet(s) approved. +TimesheetWeekMassRefuseSuccess = %s timesheet(s) refused. +TimesheetWeekMassSealSuccess = %s timesheet(s) sealed. +TimesheetWeekMassActionErrors = Unable to process: %s. +TimesheetWeekMassDeleteOnlyDraft = Only draft timesheets can be deleted in bulk. +GenerateSummaryPdf = Generate summary PDF +TimesheetWeekSummaryNoSelection = Please select at least one timesheet to create the summary. +TimesheetWeekSummaryUnauthorizedSheet = Some selected timesheets were ignored because you are not allowed to read them. +TimesheetWeekSummaryMissingUser = The employee linked to a selected timesheet no longer exists. +TimesheetWeekSummaryNoData = No readable timesheet was found in the selection. +TimesheetWeekSummaryTitle = Weekly timesheets summary +TimesheetWeekSummaryGeneratedOn = Generated on %s +TimesheetWeekSummaryGeneratedOnBy = Generated on %s by %s +TimesheetWeekSummaryColumnWeek = Week +TimesheetWeekSummaryColumnStart = Start date +TimesheetWeekSummaryColumnEnd = End date +TimesheetWeekSummaryColumnTotalHours = Declared hours +TimesheetWeekSummaryColumnContractHours = Contract hours +TimesheetWeekSummaryColumnOvertime = Overtime +TimesheetWeekSummaryColumnMeals = Meal allowances +TimesheetWeekSummaryColumnZone1 = Zone 1 trips +TimesheetWeekSummaryColumnZone2 = Zone 2 trips +TimesheetWeekSummaryColumnZone3 = Zone 3 trips +TimesheetWeekSummaryColumnZone4 = Zone 4 trips +TimesheetWeekSummaryColumnZone5 = Zone 5 trips +TimesheetWeekSummaryColumnApprovedBy = Approved by +TimesheetWeekSummaryUserTitle = %s +TimesheetWeekSummaryTotalsLabel = Totals +TimesheetWeekSummaryTableTooTall = The summary for %s contains too many rows to fit on a single page. +TimesheetWeekSummaryGenerated = Summary PDF ready. +TimesheetWeekSummaryFilename = Weekly timesheets summary - Week %s to %s.pdf +TimesheetWeekSummaryHeaderWeekRange = Week #%1$s %2$s to %3$s %4$s + TimesheetWeekStatusDraft = Draft TimesheetWeekStatusSubmitted = Submitted TimesheetWeekStatusApproved = Approved @@ -162,6 +199,8 @@ TimesheetWeekNotificationEmployeeFallback = Employee TimesheetWeekAjaxForbidden = You are not allowed to update this timesheet. TimesheetWeekAjaxUpdateError = Unable to update field %s. TimesheetWeekAjaxUpdateSuccess = Field %s updated successfully. +TimesheetWeekConfirmMassDelete = Do you confirm the deletion of the %s selected timesheets? +TimesheetWeekErrorNoSelection = Please select at least one timesheet before launching a mass action. TimesheetWeekDayMonday = Monday TimesheetWeekDayTuesday = Tuesday TimesheetWeekDayWednesday = Wednesday diff --git a/langs/fr_FR/timesheetweek.lang b/langs/fr_FR/timesheetweek.lang index 790c3a8..2f153bb 100644 --- a/langs/fr_FR/timesheetweek.lang +++ b/langs/fr_FR/timesheetweek.lang @@ -111,6 +111,43 @@ SealTimesheet = Sceller UnsealTimesheet = Desceller ConfirmValidate = Confirmez-vous l'approbation de cette feuille de temps ? ConfirmRefuse = Confirmez-vous le refus de cette feuille de temps ? +ApproveSelection = Approuver la sélection +RefuseSelection = Refuser la sélection +SealSelection = Sceller la sélection +DeleteSelection = Supprimer la sélection +TimesheetWeekMassApproveSuccess = %s feuille(s) de temps approuvée(s). +TimesheetWeekMassRefuseSuccess = %s feuille(s) de temps refusée(s). +TimesheetWeekMassSealSuccess = %s feuille(s) de temps scellée(s). +TimesheetWeekMassActionErrors = Impossible de traiter : %s. +TimesheetWeekMassDeleteOnlyDraft = Seules les feuilles en brouillon peuvent être supprimées en masse. +GenerateSummaryPdf = Générer le PDF de synthèse +TimesheetWeekSummaryNoSelection = Merci de sélectionner au moins une feuille de temps pour créer la synthèse. +TimesheetWeekSummaryUnauthorizedSheet = Certaines feuilles sélectionnées ont été ignorées faute d'autorisation de lecture. +TimesheetWeekSummaryMissingUser = Le salarié lié à une feuille sélectionnée n'existe plus. +TimesheetWeekSummaryNoData = Aucune feuille lisible n'a été trouvée dans la sélection. +TimesheetWeekSummaryTitle = Synthèse hebdomadaire des feuilles de temps +TimesheetWeekSummaryGeneratedOn = Généré le %s +TimesheetWeekSummaryGeneratedOnBy = Généré le %s par %s +TimesheetWeekSummaryColumnWeek = Semaine +TimesheetWeekSummaryColumnStart = Date de début +TimesheetWeekSummaryColumnEnd = Date de fin +TimesheetWeekSummaryColumnTotalHours = Heures déclarées +TimesheetWeekSummaryColumnContractHours = Heures au contrat +TimesheetWeekSummaryColumnOvertime = Heures supplémentaires +TimesheetWeekSummaryColumnMeals = Paniers repas +TimesheetWeekSummaryColumnZone1 = Déplacements Zone 1 +TimesheetWeekSummaryColumnZone2 = Déplacements Zone 2 +TimesheetWeekSummaryColumnZone3 = Déplacements Zone 3 +TimesheetWeekSummaryColumnZone4 = Déplacements Zone 4 +TimesheetWeekSummaryColumnZone5 = Déplacements Zone 5 +TimesheetWeekSummaryColumnApprovedBy = Approuvé par +TimesheetWeekSummaryUserTitle = %s +TimesheetWeekSummaryTotalsLabel = Totaux +TimesheetWeekSummaryTableTooTall = La synthèse pour %s contient trop de lignes pour tenir sur une seule page. +TimesheetWeekSummaryGenerated = PDF de synthèse disponible. +TimesheetWeekSummaryFilename = Synthese hebdomadaire des feuilles de temps - Semaine %s a %s.pdf +TimesheetWeekSummaryHeaderWeekRange = Semaine n°%1$s %2$s à %3$s %4$s + TimesheetWeekStatusDraft = Brouillon TimesheetWeekStatusSubmitted = Soumise TimesheetWeekStatusApproved = Approuvée @@ -161,6 +198,8 @@ TimesheetWeekNotificationEmployeeFallback = Salarié TimesheetWeekAjaxForbidden = Vous n'êtes pas autorisé à mettre à jour cette feuille de temps. TimesheetWeekAjaxUpdateError = Impossible de mettre à jour le champ %s. TimesheetWeekAjaxUpdateSuccess = Champ %s mis à jour avec succès. +TimesheetWeekConfirmMassDelete = Confirmez-vous la suppression des %s feuilles de temps sélectionnées ? +TimesheetWeekErrorNoSelection = Veuillez sélectionner au moins une feuille de temps avant de lancer une action de masse. TimesheetWeekDayMonday = Lundi TimesheetWeekDayTuesday = Mardi TimesheetWeekDayWednesday = Mercredi diff --git a/lib/timesheetweek_pdf.lib.php b/lib/timesheetweek_pdf.lib.php new file mode 100644 index 0000000..eec15c7 --- /dev/null +++ b/lib/timesheetweek_pdf.lib.php @@ -0,0 +1,949 @@ + + * + * 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 . + */ + +/** + * \file lib/timesheetweek_pdf.lib.php + * \ingroup timesheetweek + * \brief Helper functions for TimesheetWeek PDF exports + */ + +// EN: Load Dolibarr helpers required to build PDF documents. +// FR: Charge les helpers Dolibarr nécessaires pour construire les documents PDF. +require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; + +dol_include_once('/timesheetweek/lib/timesheetweek.lib.php'); + +defined('TIMESHEETWEEK_PDF_SUMMARY_SUBDIR') || define('TIMESHEETWEEK_PDF_SUMMARY_SUBDIR', 'summaries'); + +/** + * EN: Normalise a value before sending it to TCPDF by decoding HTML entities and applying the output charset. + * FR: Normalise une valeur avant envoi à TCPDF en décodant les entités HTML et en appliquant le jeu de caractères de sortie. + * + * @param string $value + * @return string + */ +function tw_pdf_normalize_string($value) +{ + // EN: Convert any scalar input into a string for consistent processing. + // FR: Convertit toute entrée scalaire en chaîne pour un traitement cohérent. + $value = (string) $value; + if (function_exists('dol_html_entity_decode')) { + // EN: Decode HTML entities with Dolibarr helper to restore accented characters before rendering. + // FR: Décode les entités HTML avec l'helper Dolibarr pour restaurer les caractères accentués avant affichage. + $value = dol_html_entity_decode($value, ENT_QUOTES | ENT_HTML401, 'UTF-8'); + } else { + // EN: Fallback on PHP native decoder when the Dolibarr helper is unavailable. + // FR: Utilise le décodeur natif PHP si l'helper Dolibarr est indisponible. + $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML401, 'UTF-8'); + } + // EN: Access the global translations handler to convert text with the right PDF charset. + // FR: Accède au gestionnaire global de traductions pour convertir le texte avec le bon jeu de caractères PDF. + global $langs; + if ($langs instanceof Translate) { + // EN: Convert the text into the PDF output charset defined by Dolibarr translations. + // FR: Convertit le texte dans le jeu de caractères de sortie PDF défini par les traductions Dolibarr. + $value = $langs->convToOutputCharset($value); + } + // EN: Return the fully normalised value ready to be consumed by TCPDF rendering calls. + // FR: Retourne la valeur totalement normalisée, prête à être consommée par les appels de rendu TCPDF. + return $value; +} + +/** + * EN: Wrap the normalised value in a safe HTML span ready for TCPDF output. + * FR: Entoure la valeur normalisée dans un span HTML sûr prêt pour la sortie TCPDF. + * + * @param string $value + * @return string + */ +function tw_pdf_format_cell_html($value) +{ + // EN: Normalise the input before escaping to keep accents visible in PDF cells. + // FR: Normalise la valeur avant échappement pour conserver les accents visibles dans les cellules PDF. + $normalizedValue = tw_pdf_normalize_string($value); + // EN: Escape HTML special characters using Dolibarr helper to protect TCPDF output. + // FR: Échappe les caractères spéciaux HTML avec l'helper Dolibarr pour sécuriser la sortie TCPDF. + $escapedValue = dol_escape_htmltag($normalizedValue); + // EN: Wrap the escaped value in a span to stay compatible with TCPDF HTML rendering expectations. + // FR: Encapsule la valeur échappée dans un span pour rester compatible avec les attentes de rendu HTML de TCPDF. + return ''.$escapedValue.''; +} + +/** + * EN: Render the Dolibarr-styled header containing the logo and company name. + * FR: Dessine l'entête au style Dolibarr avec le logo et le nom de l'entreprise. + * + * @param TCPDF $pdf + * @param Translate $langs + * @param Conf $conf + * @param float $leftMargin + * @param float $topMargin + * @param string $title + * @param string $weekRange + * @param string $subtitle + * @return float + */ +function tw_pdf_draw_header($pdf, $langs, $conf, $leftMargin, $topMargin, $title = '', $weekRange = '', $subtitle = '') +{ + global $mysoc; + + $defaultFontSize = pdf_getPDFFontSize($langs); + $logoHeight = 0.0; + $posX = $leftMargin; + $posY = $topMargin; + $logoPath = ''; + $logoDisplayed = false; + $pageWidth = $pdf->getPageWidth(); + $margins = method_exists($pdf, 'getMargins') ? (array) $pdf->getMargins() : array(); + $rightMargin = isset($margins['right']) ? (float) $margins['right'] : (float) getDolGlobalInt('MAIN_PDF_MARGIN_RIGHT', 10); + $rightBlockWidth = max(90.0, $pageWidth * 0.28); + $rightBlockX = max($leftMargin, $pageWidth - $rightMargin - $rightBlockWidth); + $rightBlockBottom = $posY; + + if (!getDolGlobalInt('PDF_DISABLE_MYCOMPANY_LOGO')) { + // EN: Resolve the preferred logo file between large and thumbnail versions. + // FR: Résout le fichier logo privilégié entre les versions grande et miniature. + if (!empty($mysoc->logo)) { + $logodir = $conf->mycompany->dir_output; + if (!empty($conf->mycompany->multidir_output[$conf->entity] ?? null)) { + $logodir = $conf->mycompany->multidir_output[$conf->entity]; + } + if (!getDolGlobalInt('MAIN_PDF_USE_LARGE_LOGO') && !empty($mysoc->logo_small)) { + $logoCandidate = $logodir.'/logos/thumbs/'.$mysoc->logo_small; + if (is_readable($logoCandidate)) { + $logoPath = $logoCandidate; + } + } + if ($logoPath === '') { + $logoCandidate = $logodir.'/logos/'.$mysoc->logo; + if (is_readable($logoCandidate)) { + $logoPath = $logoCandidate; + } + } + } + if ($logoPath === '') { + $defaultLogo = DOL_DOCUMENT_ROOT.'/theme/dolibarr_logo.svg'; + if (is_readable($defaultLogo)) { + $logoPath = $defaultLogo; + } + } + if ($logoPath !== '') { + $logoHeight = pdf_getHeightForLogo($logoPath); + $pdf->Image($logoPath, $posX, $posY, 0, $logoHeight); + // EN: Track that a logo is displayed to hide the company name for visual consistency. + // FR: Indique qu'un logo est affiché pour masquer le nom de la société et préserver la cohérence visuelle. + $logoDisplayed = true; + } + } + + $companyName = !empty($mysoc->name) ? $mysoc->name : 'Dolibarr ERP & CRM'; + $leftBlockWidth = max(60.0, $rightBlockX - $posX - 2.0); + if (!$logoDisplayed) { + // EN: Show the company name only when no logo is available to avoid duplicate branding. + // FR: Affiche le nom de la société uniquement lorsqu'aucun logo n'est disponible pour éviter une double identité visuelle. + $pdf->SetTextColor(0, 0, 60); + $pdf->SetFont('', 'B', $defaultFontSize); + $pdf->SetXY($posX, $posY + max($logoHeight - 6.0, 0.0)); + $pdf->MultiCell($leftBlockWidth, 5, tw_pdf_format_cell_html($companyName), 0, 'L', 0, 1, '', '', true, 0, true); + } + + // EN: Render the summary title and metadata within the right column of the header. + // FR: Affiche le titre de synthèse et les métadonnées dans la colonne droite de l'entête. + // EN: Remove unnecessary spaces around the header title for accurate checks. + // FR: Supprime les espaces superflus autour du titre d'entête pour des vérifications précises. + $trimmedTitle = trim((string) $title); + if (dol_strlen($trimmedTitle) > 0) { + $pdf->SetFont('', 'B', $defaultFontSize + 2); + $pdf->SetTextColor(0, 0, 60); + $pdf->SetXY($rightBlockX, $posY); + // EN: Force the header title to stay on a single line for a cleaner top-right layout. + // FR: Force le titre d'entête à rester sur une seule ligne pour un rendu plus propre en haut à droite. + $pdf->Cell($rightBlockWidth, 6, tw_pdf_normalize_string($trimmedTitle), 0, 0, 'R', 0, '', 0, false, 'T', 'T'); + $rightBlockBottom = max($rightBlockBottom, $posY + 6.0); + } + // EN: Trim the ISO week range label before output. + // FR: Supprime les espaces du libellé de plage de semaines avant affichage. + $trimmedWeekRange = trim((string) $weekRange); + if (dol_strlen($trimmedWeekRange) > 0) { + $pdf->SetFont('', '', $defaultFontSize); + $pdf->SetTextColor(0, 0, 0); + $pdf->SetXY($rightBlockX, $rightBlockBottom + 1.0); + // EN: Show the ISO week range immediately below the title to mirror Dolibarr headers. + // FR: Affiche la plage de semaines ISO juste sous le titre pour refléter les entêtes Dolibarr. + $pdf->MultiCell($rightBlockWidth, 5, tw_pdf_format_cell_html($trimmedWeekRange), 0, 'R', 0, 1, '', '', true, 0, true); + $rightBlockBottom = max($rightBlockBottom, $pdf->GetY()); + } + // EN: Remove unnecessary spaces around the header subtitle before rendering. + // FR: Supprime les espaces superflus autour du sous-titre d'entête avant affichage. + $trimmedSubtitle = trim((string) $subtitle); + if (dol_strlen($trimmedSubtitle) > 0) { + $pdf->SetFont('', '', $defaultFontSize); + $pdf->SetTextColor(0, 0, 0); + $pdf->SetXY($rightBlockX, $rightBlockBottom + 1.0); + $pdf->MultiCell($rightBlockWidth, 5, tw_pdf_format_cell_html($trimmedSubtitle), 0, 'R', 0, 1, '', '', true, 0, true); + $rightBlockBottom = max($rightBlockBottom, $pdf->GetY()); + } + + $pdf->SetTextColor(0, 0, 0); + + return max($posY + max($logoHeight, 16.0), $rightBlockBottom); +} + +/** + * EN: Draw the footer using the standard Dolibarr helper to keep consistent branding. + * FR: Dessine le pied de page avec le helper Dolibarr standard pour conserver la charte. + * + * @param TCPDF $pdf + * @param Translate $langs + * @param Conf $conf + * @param float $leftMargin + * @param float $rightMargin + * @param float $bottomMargin + * @param CommonObject|null $object + * @param int $hideFreeText + * @param float|null $autoPageBreakMargin + * @return int + */ +function tw_pdf_draw_footer($pdf, $langs, $conf, $leftMargin, $rightMargin, $bottomMargin, $object = null, $hideFreeText = 0, $autoPageBreakMargin = null) +{ + global $mysoc; + + // EN: Backup automatic page break configuration to avoid splitting the footer on two pages. + // FR: Sauvegarde la configuration de saut automatique pour éviter de scinder le pied entre deux pages. + $previousAutoBreak = method_exists($pdf, 'getAutoPageBreak') ? $pdf->getAutoPageBreak() : true; + $previousBreakMargin = null; + if (method_exists($pdf, 'getBreakMargin')) { + $previousBreakMargin = (float) $pdf->getBreakMargin(); + } elseif ($autoPageBreakMargin !== null) { + $previousBreakMargin = (float) $autoPageBreakMargin; + } elseif (isset($pdf->bMargin)) { + // EN: Fallback on TCPDF public margin when helper methods are unavailable. + // FR: Utilise la marge publique de TCPDF si les helpers sont indisponibles. + $previousBreakMargin = (float) $pdf->bMargin; + } else { + $previousBreakMargin = (float) $bottomMargin; + } + $pdf->SetAutoPageBreak(false, 0); + + // EN: Determine if Dolibarr must show the detailed footer blocks (tax numbers, contacts, ...). + // FR: Détermine si Dolibarr doit afficher les blocs détaillés du pied (numéros fiscaux, contacts, ...). + $showDetails = empty($conf->global->MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS) ? 0 : $conf->global->MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS; + + // EN: Delegate the rendering to pdf_pagefoot to mirror the official Dolibarr layout and logic. + // FR: Délègue le rendu à pdf_pagefoot pour reproduire la mise en forme et la logique officielles de Dolibarr. + $footHeight = pdf_pagefoot($pdf, $langs, 'INVOICE_FREE_TEXT', $mysoc, $bottomMargin, $leftMargin, $pdf->getPageHeight(), $object, $showDetails, $hideFreeText); + + // EN: Restore the automatic page break configuration so the following content keeps the same flow. + // FR: Restaure la configuration de saut automatique pour conserver le même flux pour le contenu suivant. + $pdf->SetAutoPageBreak($previousAutoBreak, $previousBreakMargin); + + return $footHeight; +} + + +/** + * EN: Create a new landscape page and ensure header/footer are drawn. + * FR: Crée une nouvelle page paysage et dessine l'entête/pied de page. + * + * @param TCPDF $pdf + * @param Translate $langs + * @param Conf $conf + * @param float $leftMargin + * @param float $topMargin + * @param float $rightMargin + * @param float $bottomMargin + * @param float|null $autoPageBreakMargin + * @param string $headerTitle + * @param string $headerWeekRange + * @param string $headerSubtitle + * @return float + */ +function tw_pdf_add_landscape_page($pdf, $langs, $conf, $leftMargin, $topMargin, $rightMargin, $bottomMargin, &$headerState = null, $autoPageBreakMargin = null, $headerTitle = '', $headerWeekRange = '', $headerSubtitle = '') +{ + $pdf->AddPage('L'); + // EN: Detect if TCPDF automatic callbacks manage header/footer rendering. + // FR: Détecte si les callbacks automatiques de TCPDF gèrent le rendu entête/pied. + $callbacksOn = is_array($headerState) && !empty($headerState['automatic']); + + if ($callbacksOn) { + // EN: Recompute the header height when missing to avoid duplicated footer calls. + // FR: Recalcule la hauteur d'entête lorsqu'elle manque pour éviter les appels de pied dupliqués. + $headerBottom = !empty($headerState['value']) + ? (float) $headerState['value'] + : tw_pdf_draw_header($pdf, $langs, $conf, $leftMargin, $topMargin, $headerTitle, $headerWeekRange, $headerSubtitle); + } else { + $headerBottom = tw_pdf_draw_header($pdf, $langs, $conf, $leftMargin, $topMargin, $headerTitle, $headerWeekRange, $headerSubtitle); + tw_pdf_draw_footer($pdf, $langs, $conf, $leftMargin, $rightMargin, $bottomMargin, null, 0, $autoPageBreakMargin); + if (is_array($headerState)) { + // EN: Store the header height for further pages when callbacks remain disabled. + // FR: Mémorise la hauteur d'entête pour les prochaines pages lorsque les callbacks restent inactifs. + $headerState['value'] = $headerBottom; + } + } + $contentStart = $headerBottom + 4.0; + // EN: Force the top margin below the header so every page keeps data between header and footer. + // FR: Force la marge haute sous l'entête pour que chaque page maintienne les données entre entête et pied. + $pdf->SetTopMargin($contentStart); + $pdf->SetXY($leftMargin, $contentStart); + return $contentStart; +} + +/** + * EN: Display the employee banner for the current section on the PDF. + * FR: Affiche la bannière de l'employé pour la section courante du PDF. + * + * @param TCPDF $pdf + * @param Translate $langs + * @param User $userObject + * @param float $defaultFontSize + * @return void + */ +function tw_pdf_print_user_banner($pdf, $langs, $userObject, $defaultFontSize) +{ + $pdf->SetFont('', 'B', $defaultFontSize + 1); + $pdf->SetTextColor(0, 0, 60); + $pdf->MultiCell(0, 6, tw_pdf_format_cell_html($langs->trans('TimesheetWeekSummaryUserTitle', $userObject->getFullName($langs))), 0, 'L', 0, 1, '', '', true, 0, true); + $pdf->SetFont('', '', $defaultFontSize); + $pdf->SetTextColor(0, 0, 0); +} + + +/** + * EN: Determine the height required for a table row considering wrapped content. + * FR: Détermine la hauteur nécessaire pour une ligne du tableau en considérant les retours à la ligne. + * + * @param TCPDF $pdf + * @param float[] $columnWidths + * @param string[] $values + * @param float $lineHeight + * @return float + */ +function tw_pdf_estimate_row_height($pdf, array $columnWidths, array $values, $lineHeight) +{ + // EN: Track the maximum number of lines across the row to harmonise heights. + // FR: Suit le nombre maximal de lignes pour harmoniser les hauteurs. + $maxLines = 1; + foreach ($values as $index => $value) { + $text = tw_pdf_format_cell_html($value); + $plain = dol_string_nohtmltag($text); + $currentLines = max(1, $pdf->getNumLines($plain, $columnWidths[$index])); + $maxLines = max($maxLines, $currentLines); + } + return $lineHeight * $maxLines; +} + +/** + * EN: Render a row with uniform cell height and consistent column widths. + * FR: Affiche une ligne avec une hauteur uniforme des cellules et des largeurs cohérentes par colonne. + * + * @param TCPDF $pdf + * @param float[] $columnWidths + * @param string[] $values + * @param float $lineHeight + * @param array $options + * @return void + */ +function tw_pdf_render_row($pdf, array $columnWidths, array $values, $lineHeight, array $options = array()) +{ + $border = $options['border'] ?? 1; + $fill = !empty($options['fill']); + $alignments = $options['alignments'] ?? array(); + // EN: Compute the shared height to align every cell in the row. + // FR: Calcule la hauteur commune pour aligner toutes les cellules de la ligne. + $rowHeight = tw_pdf_estimate_row_height($pdf, $columnWidths, $values, $lineHeight); + $initialX = $pdf->GetX(); + $initialY = $pdf->GetY(); + $offset = 0.0; + foreach ($values as $index => $value) { + $width = $columnWidths[$index]; + $align = $alignments[$index] ?? 'L'; + // EN: Position each cell manually to guarantee column alignment. + // FR: Positionne chaque cellule manuellement pour garantir l'alignement des colonnes. + $pdf->SetXY($initialX + $offset, $initialY); + $pdf->MultiCell( + $width, + $rowHeight, + tw_pdf_format_cell_html($value), + $border, + $align, + $fill, + 0, + '', + '', + true, + 0, + true, + true, + $rowHeight, + 'T', + false + ); + $offset += $width; + } + // EN: Move the cursor under the row for the next drawing operations. + // FR: Replace le curseur sous la ligne pour les prochaines opérations de dessin. + $pdf->SetXY($initialX, $initialY + $rowHeight); +} + +/** + * EN: Estimate the full height required to display a user table without page breaks. + * FR: Estime la hauteur complète nécessaire pour afficher un tableau utilisateur sans saut de page. + * + * @param TCPDF $pdf + * @param Translate $langs + * @param User $userObject + * @param float[] $columnWidths + * @param string[] $columnLabels + * @param string[][] $recordRows + * @param string[] $totalsRow + * @param float $lineHeight + * @param float $contentWidth + * @return float + */ +function tw_pdf_estimate_user_table_height($pdf, $langs, $userObject, array $columnWidths, array $columnLabels, array $recordRows, array $totalsRow, $lineHeight, $contentWidth) +{ + $bannerText = $langs->trans('TimesheetWeekSummaryUserTitle', $userObject->getFullName($langs)); + $bannerPlain = dol_string_nohtmltag(tw_pdf_format_cell_html($bannerText)); + // EN: Evaluate banner height using the same line width as the MultiCell call. + // FR: Évalue la hauteur de la bannière en utilisant la même largeur de ligne que l'appel MultiCell. + $bannerLines = max(1, $pdf->getNumLines($bannerPlain, $contentWidth)); + $bannerHeight = 6 * $bannerLines; + + // EN: Account for the spacing introduced before the table header. + // FR: Prend en compte l'espacement introduit avant l'entête du tableau. + $headerHeight = tw_pdf_estimate_row_height($pdf, $columnWidths, $columnLabels, $lineHeight); + $totalHeight = $bannerHeight + 2 + $headerHeight; + + foreach ($recordRows as $rowValues) { + $totalHeight += tw_pdf_estimate_row_height($pdf, $columnWidths, $rowValues, $lineHeight); + } + + $totalHeight += tw_pdf_estimate_row_height($pdf, $columnWidths, $totalsRow, $lineHeight); + + return $totalHeight; +} + +/** + * EN: Format decimal hours into the HH:MM representation expected by HR teams. + * FR: Formate les heures décimales en représentation HH:MM attendue par les équipes RH. + * + * @param float $hours + * @return string + */ +function tw_format_hours_decimal($hours) +{ + $hours = (float) $hours; + $hoursInt = (int) floor($hours); + $minutes = (int) round(($hours - $hoursInt) * 60); + if ($minutes === 60) { + $hoursInt++; + $minutes = 0; + } + return sprintf('%02d:%02d', $hoursInt, $minutes); +} + +/** + * EN: Build the dataset required to generate a PDF summary of weekly timesheets. + * FR: Construit l'ensemble de données nécessaire pour générer un résumé PDF des feuilles hebdomadaires. + * + * @param DoliDB $db Database handler + * @param int[] $timesheetIds Selected timesheet identifiers + * @param User $user Current Dolibarr user + * @param bool $permReadOwn Permission to read own sheets + * @param bool $permReadChild Permission to read subordinates sheets + * @param bool $permReadAll Permission to read all sheets + * @return array{users:array>,totals:array>>,errors:string[]>| + * array{errors:string[]} + */ +function tw_collect_summary_data($db, array $timesheetIds, User $user, $permReadOwn, $permReadChild, $permReadAll) +{ + $ids = array(); + foreach ($timesheetIds as $candidate) { + $candidate = (int) $candidate; + if ($candidate > 0) { + $ids[] = $candidate; + } + } + $ids = array_values(array_unique($ids)); + if (empty($ids)) { + return array('errors' => array('TimesheetWeekSummaryNoSelection')); + } + + $idList = implode(',', $ids); +$sql = "SELECT t.rowid, t.entity, t.year, t.week, t.total_hours, t.overtime_hours, t.zone1_count, t.zone2_count, t.zone3_count, t.zone4_count, t.zone5_count, t.meal_count, t.fk_user, t.fk_user_valid, u.lastname, u.firstname, u.weeklyhours, uv.lastname as validator_lastname, uv.firstname as validator_firstname"; +$sql .= " FROM ".MAIN_DB_PREFIX."timesheet_week as t"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON u.rowid = t.fk_user"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as uv ON uv.rowid = t.fk_user_valid"; + $sql .= " WHERE t.rowid IN (".$idList.")"; + $sql .= " AND t.entity IN (".getEntity('timesheetweek').")"; + + $resql = $db->query($sql); + if (!$resql) { + return array('errors' => array($db->lasterror())); + } + + $dataset = array(); + $errors = array(); + + while ($row = $db->fetch_object($resql)) { + $targetUserId = (int) $row->fk_user; + $canRead = tw_can_act_on_user($targetUserId, $permReadOwn, $permReadChild, ($permReadAll || !empty($user->admin)), $user); + if (!$canRead) { + $errors[] = 'TimesheetWeekSummaryUnauthorizedSheet'; + continue; + } + + $week = (int) $row->week; + $year = (int) $row->year; + + $weekStart = new DateTime(); + $weekStart->setISODate($year, $week); + $weekEnd = clone $weekStart; + $weekEnd->modify('+6 days'); + + $contractHours = (float) $row->weeklyhours; + if ($contractHours <= 0) { + $contractHours = 35.0; + } + $contractHours = min($contractHours, (float) $row->total_hours); + + if (!isset($dataset[$targetUserId])) { + $userSummary = new User($db); + if ($userSummary->fetch($targetUserId) <= 0) { + // EN: Skip users that cannot be fetched due to deletion or entity mismatch. + // FR: Ignore les utilisateurs introuvables suite à une suppression ou à un écart d'entité. + $errors[] = 'TimesheetWeekSummaryMissingUser'; + continue; + } + $dataset[$targetUserId] = array( + 'user' => $userSummary, + 'records' => array(), + 'totals' => array( + 'total_hours' => 0.0, + 'contract_hours' => 0.0, + 'overtime_hours' => 0.0, + 'meal_count' => 0, + 'zone1_count' => 0, + 'zone2_count' => 0, + 'zone3_count' => 0, + 'zone4_count' => 0, + 'zone5_count' => 0 + ) + ); + } + +$approvedBy = ''; +if (!empty($row->validator_lastname) || !empty($row->validator_firstname)) { +// EN: Build the approver full name respecting Dolibarr formatting. +// FR: Construit le nom complet de l'approbateur selon le format Dolibarr. +$approvedBy = dolGetFirstLastname($row->validator_firstname, $row->validator_lastname); +} + +$record = array( +'id' => (int) $row->rowid, +'week' => $week, +'year' => $year, +'week_start' => $weekStart, +'week_end' => $weekEnd, +'total_hours' => (float) $row->total_hours, +'contract_hours' => (float) $contractHours, +'overtime_hours' => (float) $row->overtime_hours, +'meal_count' => (int) $row->meal_count, +'zone1_count' => (int) $row->zone1_count, +'zone2_count' => (int) $row->zone2_count, +'zone3_count' => (int) $row->zone3_count, +'zone4_count' => (int) $row->zone4_count, +'zone5_count' => (int) $row->zone5_count, +'approved_by' => $approvedBy +); + + $dataset[$targetUserId]['records'][] = $record; + $dataset[$targetUserId]['totals']['total_hours'] += $record['total_hours']; + $dataset[$targetUserId]['totals']['contract_hours'] += $record['contract_hours']; + $dataset[$targetUserId]['totals']['overtime_hours'] += $record['overtime_hours']; + $dataset[$targetUserId]['totals']['meal_count'] += $record['meal_count']; + $dataset[$targetUserId]['totals']['zone1_count'] += $record['zone1_count']; + $dataset[$targetUserId]['totals']['zone2_count'] += $record['zone2_count']; + $dataset[$targetUserId]['totals']['zone3_count'] += $record['zone3_count']; + $dataset[$targetUserId]['totals']['zone4_count'] += $record['zone4_count']; + $dataset[$targetUserId]['totals']['zone5_count'] += $record['zone5_count']; + } + + $db->free($resql); + + if (empty($dataset)) { + $errors[] = 'TimesheetWeekSummaryNoData'; + } + + return array( + 'users' => $dataset, + 'errors' => array_values(array_unique($errors)) + ); +} + +/** + * EN: Generate the PDF file summarising selected weekly timesheets. + * FR: Génère le fichier PDF résumant les feuilles de temps hebdomadaires sélectionnées. + * + * @param DoliDB $db Database handler + * @param Conf $conf Dolibarr configuration + * @param Translate $langs Translator + * @param User $user Current Dolibarr user + * @param int[] $timesheetIds Selected timesheet identifiers + * @param bool $permReadOwn Permission to read own sheets + * @param bool $permReadChild Permission to read subordinates sheets + * @param bool $permReadAll Permission to read all sheets + * @return array{success:bool,file?:string,relative?:string,errors?:string[],warnings?:string[]} + */ +function tw_generate_summary_pdf($db, $conf, $langs, User $user, array $timesheetIds, $permReadOwn, $permReadChild, $permReadAll) +{ + // EN: Ensure all translations required by the PDF summary are available before rendering. + // FR: Garantit la disponibilité des traductions nécessaires à la synthèse PDF avant le rendu. + if (method_exists($langs, 'loadLangs')) { + $langs->loadLangs(array('timesheetweek@timesheetweek', 'errors')); + } else { + // EN: Fallback for older Dolibarr versions that expose only the singular loader. + // FR: Solution de secours pour les versions de Dolibarr ne proposant que le chargeur unitaire. + $langs->load('timesheetweek@timesheetweek'); + $langs->load('errors'); + } + + // EN: Guarantee the Dolibarr "main" dictionary is available even when not preloaded. + // FR: Garantit la disponibilité du dictionnaire « main » de Dolibarr même s'il n'est pas préchargé. + if (!property_exists($langs, 'loadedlangs') || empty($langs->loadedlangs['main'])) { + $langs->load('main'); + } + + // EN: Guarantee the Dolibarr "companies" dictionary is available to translate company details. + // FR: Garantit la disponibilité du dictionnaire « companies » pour traduire les informations société. + if (!property_exists($langs, 'loadedlangs') || empty($langs->loadedlangs['companies'])) { + $langs->load('companies'); + } + + $dataResult = tw_collect_summary_data($db, $timesheetIds, $user, $permReadOwn, $permReadChild, $permReadAll); + $rawWarnings = !empty($dataResult['errors']) ? $dataResult['errors'] : array(); + $warnings = array(); + foreach ($rawWarnings as $warn) { + if ($warn === null) { + continue; + } + $warnings[] = $langs->trans($warn); + } + $warnings = array_values(array_unique(array_filter($warnings))); + + if (empty($dataResult['users'])) { + return array('success' => false, 'errors' => (!empty($warnings) ? $warnings : array($langs->trans('TimesheetWeekSummaryNoData')))); + } + + $dataset = $dataResult['users']; + if (empty($dataset)) { + return array('success' => false, 'errors' => (!empty($warnings) ? $warnings : array($langs->trans('TimesheetWeekSummaryNoData')))); + } + + uasort($dataset, function ($a, $b) { + $aName = dol_string_nohtmltag($a['user']->lastname); + $bName = dol_string_nohtmltag($b['user']->lastname); + return strcasecmp($aName, $bName); + }); + $sortedUsers = array_values($dataset); + + // EN: Track the lowest and highest ISO weeks among the selected records for filename generation. + // FR: Suit les semaines ISO minimale et maximale parmi les enregistrements sélectionnés pour le nom du fichier. + $earliestWeek = null; + $latestWeek = null; + foreach ($sortedUsers as $userSummary) { + if (empty($userSummary['records']) || !is_array($userSummary['records'])) { + continue; + } + foreach ($userSummary['records'] as $record) { + if (!isset($record['week']) || !isset($record['year'])) { + continue; + } + $weekValue = (int) $record['week']; + $yearValue = (int) $record['year']; + $compositeKey = sprintf('%04d%02d', $yearValue, $weekValue); + if ($earliestWeek === null || strcmp($compositeKey, $earliestWeek['key']) < 0) { + $earliestWeek = array( + 'key' => $compositeKey, + 'week' => $weekValue, + 'year' => $yearValue + ); + } + if ($latestWeek === null || strcmp($compositeKey, $latestWeek['key']) > 0) { + $latestWeek = array( + 'key' => $compositeKey, + 'week' => $weekValue, + 'year' => $yearValue + ); + } + } + } + $firstWeekLabel = $earliestWeek !== null ? sprintf('%02d', $earliestWeek['week']) : '00'; + $lastWeekLabel = $latestWeek !== null ? sprintf('%02d', $latestWeek['week']) : $firstWeekLabel; + // EN: Derive the ISO years associated with the boundary weeks for display. + // FR: Détermine les années ISO associées aux semaines limites pour l'affichage. + $firstWeekYear = $earliestWeek !== null ? sprintf('%04d', $earliestWeek['year']) : date('Y'); + $lastWeekYear = $latestWeek !== null ? sprintf('%04d', $latestWeek['year']) : $firstWeekYear; + + $uploaddir = !empty($conf->timesheetweek->multidir_output[$conf->entity] ?? null) + ? $conf->timesheetweek->multidir_output[$conf->entity] + : (!empty($conf->timesheetweek->dir_output) ? $conf->timesheetweek->dir_output : DOL_DATA_ROOT.'/timesheetweek'); + + $targetDir = rtrim($uploaddir, '/').'/'.TIMESHEETWEEK_PDF_SUMMARY_SUBDIR; + if (dol_mkdir($targetDir) < 0) { + return array('success' => false, 'errors' => array($langs->trans('ErrorCanNotCreateDir', $targetDir))); + } + + $timestamp = dol_now(); + // EN: Generate the human-readable filename using translations before sanitising it for storage. + // FR: Génère le nom lisible via les traductions avant de le nettoyer pour l'enregistrement. + $displayFilename = $langs->trans('TimesheetWeekSummaryFilename', $firstWeekLabel, $lastWeekLabel); + // EN: Remove accents and special characters before running Dolibarr sanitisation while keeping readable spaces. + // FR: Supprime les accents et caractères spéciaux avant l'assainissement Dolibarr tout en conservant des espaces lisibles. + $asciiFilename = dol_string_unaccent($displayFilename); + $asciiFilename = preg_replace('/[^A-Za-z0-9._\\- ]+/', '', $asciiFilename); + $asciiFilename = trim(preg_replace('/\s+/', ' ', $asciiFilename)); + if ($asciiFilename !== '') { + // EN: Reuse the cleaned ASCII string to preserve natural spacing in the filename. + // FR: Réutilise la chaîne ASCII nettoyée pour préserver les espaces naturels dans le nom de fichier. + $displayFilename = $asciiFilename; + } + // EN: Sanitize the filename to match Dolibarr's document security checks and avoid missing file errors. + // FR: Nettoie le nom de fichier pour correspondre aux contrôles de sécurité Dolibarr et éviter les erreurs d'absence de fichier. + $filename = dol_sanitizeFileName($displayFilename); + if ($filename === '') { + // EN: Fallback on the original label when sanitisation returns an empty value (extreme edge cases). + // FR: Revient au libellé initial si le nettoyage renvoie une valeur vide (cas extrêmes). + $filename = dol_sanitizeFileName('timesheetweek-summary-'.$firstWeekLabel.'-'.$lastWeekLabel.'.pdf'); + } + $filepath = $targetDir.'/'.$filename; + + // EN: Prepare the title and metadata strings reused inside the header block. + // FR: Prépare les libellés du titre et des métadonnées réemployés dans l'entête. + $headerTitle = $langs->trans('TimesheetWeekSummaryTitle'); + // EN: Build the human-readable ISO week range displayed under the title. + // FR: Construit la plage de semaines ISO lisible affichée sous le titre. + $headerWeekRangeLabel = $langs->trans('TimesheetWeekSummaryHeaderWeekRange'); + // EN: Inject the selected ISO week bounds into the translated label to reflect the chosen range. + // FR: Injecte les bornes de semaine ISO sélectionnées dans le libellé traduit pour refléter la plage choisie. + $headerWeekRange = sprintf($headerWeekRangeLabel, $firstWeekLabel, $firstWeekYear, $lastWeekLabel, $lastWeekYear); + $headerSubtitle = $langs->trans('TimesheetWeekSummaryGeneratedOnBy', dol_print_date($timestamp, 'dayhour'), $user->getFullName($langs)); + + $format = pdf_getFormat(); + $pdfFormat = array($format['width'], $format['height']); + $margeGauche = getDolGlobalInt('MAIN_PDF_MARGIN_LEFT', 10); + $margeDroite = getDolGlobalInt('MAIN_PDF_MARGIN_RIGHT', 10); + $margeHaute = getDolGlobalInt('MAIN_PDF_MARGIN_TOP', 10); + $margeBasse = getDolGlobalInt('MAIN_PDF_MARGIN_BOTTOM', 10); + $footerReserve = 12; + // EN: Compute the effective auto-break margin to guarantee enough room for the full footer block. + // FR: Calcule la marge effective des sauts automatiques pour réserver l'espace complet du pied de page. + $autoPageBreakMargin = $margeBasse + $footerReserve; + + $pdf = pdf_getInstance($pdfFormat); + $defaultFontSize = pdf_getPDFFontSize($langs); + $pdf->SetPageOrientation('L'); + $pdf->SetAutoPageBreak(true, $autoPageBreakMargin); + $pdf->SetMargins($margeGauche, $margeHaute, $margeDroite); + $headerState = array('value' => 0.0, 'automatic' => false); + if (method_exists($pdf, 'setHeaderCallback') && method_exists($pdf, 'setFooterCallback')) { + // EN: Delegate header rendering to TCPDF so every page created by the engine receives it automatically. + // FR: Confie le rendu de l'entête à TCPDF afin que chaque page créée par le moteur le reçoive automatiquement. + $pdf->setPrintHeader(true); + $pdf->setHeaderCallback(function ($pdfInstance) use ($langs, $conf, $margeGauche, $margeHaute, &$headerState, $headerTitle, $headerWeekRange, $headerSubtitle) { + $headerState['value'] = tw_pdf_draw_header($pdfInstance, $langs, $conf, $margeGauche, $margeHaute, $headerTitle, $headerWeekRange, $headerSubtitle); + $headerState['automatic'] = true; + }); + // EN: Delegate footer drawing to TCPDF to guarantee presence on automatic page breaks. + // FR: Confie le dessin du pied de page à TCPDF pour garantir sa présence lors des sauts automatiques. + $pdf->setPrintFooter(true); + $pdf->setFooterCallback(function ($pdfInstance) use ($langs, $conf, $margeGauche, $margeDroite, $margeBasse, $autoPageBreakMargin) { + tw_pdf_draw_footer($pdfInstance, $langs, $conf, $margeGauche, $margeDroite, $margeBasse, null, 0, $autoPageBreakMargin); + }); + } else { + // EN: Disable default TCPDF decorations when callbacks are unavailable and rely on manual drawing. + // FR: Désactive les décorations TCPDF par défaut si les callbacks sont indisponibles et s'appuie sur le dessin manuel. + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + } + // EN: Enable alias replacement for total pages when the method exists on the PDF engine. + // FR: Active le remplacement de l'alias pour le nombre total de pages quand la méthode existe sur le moteur PDF. + if (method_exists($pdf, 'AliasNbPages')) { + $pdf->AliasNbPages(); + } elseif (method_exists($pdf, 'setAliasNbPages')) { + // EN: Fallback for engines exposing the alias configuration through a setter. + // FR: Solution de secours pour les moteurs exposant la configuration de l'alias via un setter. + $pdf->setAliasNbPages(); + } + $pdf->SetCreator('Dolibarr '.DOL_VERSION); + $pdf->SetAuthor($user->getFullName($langs)); + $pdf->SetTitle(tw_pdf_normalize_string($langs->trans('TimesheetWeekSummaryTitle'))); + $pdf->SetSubject(tw_pdf_normalize_string($langs->trans('TimesheetWeekSummaryTitle'))); + $pdf->SetFont(pdf_getPDFFont($langs), '', $defaultFontSize); + $pdf->Open(); + $contentTop = tw_pdf_add_landscape_page($pdf, $langs, $conf, $margeGauche, $margeHaute, $margeDroite, $margeBasse, $headerState, $autoPageBreakMargin, $headerTitle, $headerWeekRange, $headerSubtitle); + $pageHeight = $pdf->getPageHeight(); + + // EN: Offset the cursor slightly below the header to leave breathing space before the content. + // FR: Décale le curseur juste sous l'entête pour laisser un espace avant le contenu. + $contentTop += 2.0; + $pdf->SetXY($margeGauche, $contentTop); + $pdf->SetFont('', '', $defaultFontSize); + $pdf->SetTextColor(0, 0, 0); + + $columnWidthWeights = array(14, 20, 20, 16, 18, 18, 14, 11, 11, 11, 11, 11, 24); + $columnWidths = $columnWidthWeights; + $usableWidth = $pdf->getPageWidth() - $margeGauche - $margeDroite; + $widthSum = array_sum($columnWidthWeights); + if ($widthSum > 0 && $usableWidth > 0) { + // EN: Scale each column proportionally so the table spans the full printable width. + // FR: Redimensionne chaque colonne proportionnellement pour couvrir toute la largeur imprimable. + foreach ($columnWidthWeights as $index => $weight) { + $columnWidths[$index] = ($weight / $widthSum) * $usableWidth; + } + } + $columnLabels = array( + $langs->trans('TimesheetWeekSummaryColumnWeek'), + $langs->trans('TimesheetWeekSummaryColumnStart'), + $langs->trans('TimesheetWeekSummaryColumnEnd'), + $langs->trans('TimesheetWeekSummaryColumnTotalHours'), + $langs->trans('TimesheetWeekSummaryColumnContractHours'), + $langs->trans('TimesheetWeekSummaryColumnOvertime'), + $langs->trans('TimesheetWeekSummaryColumnMeals'), + $langs->trans('TimesheetWeekSummaryColumnZone1'), + $langs->trans('TimesheetWeekSummaryColumnZone2'), + $langs->trans('TimesheetWeekSummaryColumnZone3'), + $langs->trans('TimesheetWeekSummaryColumnZone4'), + $langs->trans('TimesheetWeekSummaryColumnZone5'), + $langs->trans('TimesheetWeekSummaryColumnApprovedBy') + ); + + $lineHeight = 6; + + $isFirstUser = true; + foreach ($sortedUsers as $userSummary) { + $userObject = $userSummary['user']; + $records = $userSummary['records']; + $totals = $userSummary['totals']; + + $recordRows = array(); + foreach ($records as $record) { + $recordRows[] = array( + sprintf('%d / %d', $record['week'], $record['year']), + dol_print_date($record['week_start']->getTimestamp(), 'day'), + dol_print_date($record['week_end']->getTimestamp(), 'day'), + tw_format_hours_decimal($record['total_hours']), + tw_format_hours_decimal($record['contract_hours']), + tw_format_hours_decimal($record['overtime_hours']), + (string) $record['meal_count'], + (string) $record['zone1_count'], + (string) $record['zone2_count'], + (string) $record['zone3_count'], + (string) $record['zone4_count'], + (string) $record['zone5_count'], + $record['approved_by'] + ); + } + + $totalsRow = array( + $langs->trans('TimesheetWeekSummaryTotalsLabel'), + '', + '', + tw_format_hours_decimal($totals['total_hours']), + tw_format_hours_decimal($totals['contract_hours']), + tw_format_hours_decimal($totals['overtime_hours']), + (string) $totals['meal_count'], + (string) $totals['zone1_count'], + (string) $totals['zone2_count'], + (string) $totals['zone3_count'], + (string) $totals['zone4_count'], + (string) $totals['zone5_count'], + '' + ); + +$tableHeight = tw_pdf_estimate_user_table_height($pdf, $langs, $userObject, $columnWidths, $columnLabels, $recordRows, $totalsRow, $lineHeight, $usableWidth); + $spacingBeforeTable = $isFirstUser ? 0 : 4; + $availableHeight = ($pageHeight - ($margeBasse + $footerReserve)) - $pdf->GetY(); + if (($spacingBeforeTable + $tableHeight) > $availableHeight) { + $contentTop = tw_pdf_add_landscape_page($pdf, $langs, $conf, $margeGauche, $margeHaute, $margeDroite, $margeBasse, $headerState, $autoPageBreakMargin, $headerTitle, $headerWeekRange, $headerSubtitle); + $pageHeight = $pdf->getPageHeight(); + $availableHeight = ($pageHeight - ($margeBasse + $footerReserve)) - $pdf->GetY(); + if (($spacingBeforeTable + $tableHeight) > $availableHeight) { + // EN: Warn users when a table cannot fit even on a fresh page. + // FR: Avertit les utilisateurs lorsqu'un tableau ne tient pas même sur une page vierge. + $warnings[] = $langs->trans('TimesheetWeekSummaryTableTooTall', $userObject->getFullName($langs)); + } + } + + if ($isFirstUser) { + // EN: Skip the initial spacer so the first table begins on the opening page. + // FR: Ignore l'espacement initial pour que le premier tableau démarre sur la page d'ouverture. + $isFirstUser = false; + } else { + $pdf->Ln(4); + } + + tw_pdf_print_user_banner($pdf, $langs, $userObject, $defaultFontSize); + $headerY = $pdf->GetY() + 2; + // EN: Position the table header just after the employee banner. + // FR: Positionne l'entête du tableau juste après l'en-tête salarié. + $pdf->SetY($headerY); + $pdf->SetFillColor(230, 230, 230); + $pdf->SetDrawColor(128, 128, 128); + $pdf->SetLineWidth(0.2); + $pdf->SetFont('', 'B', $defaultFontSize - 1); + $pdf->SetX($margeGauche); + // EN: Draw the header row with uniform dimensions for every column. + // FR: Dessine la ligne d'entête avec des dimensions uniformes pour chaque colonne. + tw_pdf_render_row($pdf, $columnWidths, $columnLabels, $lineHeight, array( + 'fill' => true, + 'alignments' => array_fill(0, count($columnLabels), 'C') + )); + + $pdf->SetFont('', '', $defaultFontSize - 1); + $alignments = array('C', 'C', 'C', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'L'); + // EN: Render each data row while keeping consistent heights across the table. + // FR: Affiche chaque ligne de données en conservant des hauteurs cohérentes dans le tableau. + foreach ($recordRows as $rowData) { + $pdf->SetX($margeGauche); + tw_pdf_render_row($pdf, $columnWidths, $rowData, $lineHeight, array( + 'alignments' => $alignments + )); + } + + $pdf->SetFont('', 'B', $defaultFontSize - 1); + $pdf->SetX($margeGauche); + // EN: Print the totals row with left-aligned label and right-aligned figures. + // FR: Imprime la ligne de totaux avec libellé aligné à gauche et chiffres alignés à droite. + tw_pdf_render_row($pdf, $columnWidths, $totalsRow, $lineHeight, array( + 'alignments' => array('L', 'C', 'C', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'L') + )); + + } + $pdf->Output($filepath, 'F'); + + return array( + 'success' => true, + 'file' => $filepath, + 'relative' => TIMESHEETWEEK_PDF_SUMMARY_SUBDIR.'/'.$filename, + 'warnings' => $warnings + ); +} diff --git a/timesheetweek_agenda.php b/timesheetweek_agenda.php index 9d7a621..32025b5 100644 --- a/timesheetweek_agenda.php +++ b/timesheetweek_agenda.php @@ -213,8 +213,8 @@ function tw_fetch_assigned_users_for_events($db, array $eventIds) // Set $enablepermissioncheck to 1 to enable a minimum low level of checks $enablepermissioncheck = getDolGlobalInt('TIMESHEETWEEK_ENABLE_PERMISSION_CHECK'); if ($enablepermissioncheck) { - $permissiontoread = $user->hasRight('timesheetweek', 'timesheetweek', 'read'); - $permissiontoadd = $user->hasRight('timesheetweek', 'timesheetweek', 'write'); + $permissiontoread = $user->hasRight('timesheetweek', 'read'); + $permissiontoadd = $user->hasRight('timesheetweek', 'write'); } else { $permissiontoread = 1; $permissiontoadd = 1; diff --git a/timesheetweek_card.php b/timesheetweek_card.php index 39025c6..94e8bde 100644 --- a/timesheetweek_card.php +++ b/timesheetweek_card.php @@ -78,25 +78,25 @@ function tw_translate_error($errorKey, $langs) } // ---- Permissions (nouveau modèle) ---- -$permRead = $user->hasRight('timesheetweek','timesheetweek','read'); -$permReadChild = $user->hasRight('timesheetweek','timesheetweek','readChild'); -$permReadAll = $user->hasRight('timesheetweek','timesheetweek','readAll'); +$permRead = $user->hasRight('timesheetweek','read'); +$permReadChild = $user->hasRight('timesheetweek','readChild'); +$permReadAll = $user->hasRight('timesheetweek','readAll'); -$permWrite = $user->hasRight('timesheetweek','timesheetweek','write'); -$permWriteChild = $user->hasRight('timesheetweek','timesheetweek','writeChild'); -$permWriteAll = $user->hasRight('timesheetweek','timesheetweek','writeAll'); +$permWrite = $user->hasRight('timesheetweek','write'); +$permWriteChild = $user->hasRight('timesheetweek','writeChild'); +$permWriteAll = $user->hasRight('timesheetweek','writeAll'); -$permValidate = $user->hasRight('timesheetweek','timesheetweek','validate'); -$permValidateOwn = $user->hasRight('timesheetweek','timesheetweek','validateOwn'); -$permValidateChild = $user->hasRight('timesheetweek','timesheetweek','validateChild'); -$permValidateAll = $user->hasRight('timesheetweek','timesheetweek','validateAll'); +$permValidate = $user->hasRight('timesheetweek','validate'); +$permValidateOwn = $user->hasRight('timesheetweek','validateOwn'); +$permValidateChild = $user->hasRight('timesheetweek','validateChild'); +$permValidateAll = $user->hasRight('timesheetweek','validateAll'); -$permDelete = $user->hasRight('timesheetweek','timesheetweek','delete'); -$permDeleteChild = $user->hasRight('timesheetweek','timesheetweek','deleteChild'); -$permDeleteAll = $user->hasRight('timesheetweek','timesheetweek','deleteAll'); +$permDelete = $user->hasRight('timesheetweek','delete'); +$permDeleteChild = $user->hasRight('timesheetweek','deleteChild'); +$permDeleteAll = $user->hasRight('timesheetweek','deleteAll'); -$permSeal = $user->hasRight('timesheetweek','timesheetweek','seal'); -$permUnseal = $user->hasRight('timesheetweek','timesheetweek','unseal'); +$permSeal = $user->hasRight('timesheetweek','seal'); +$permUnseal = $user->hasRight('timesheetweek','unseal'); $permReadAny = ($permRead || $permReadChild || $permReadAll); $permWriteAny = ($permWrite || $permWriteChild || $permWriteAll); diff --git a/timesheetweek_document.php b/timesheetweek_document.php index efb37f6..84d77a8 100644 --- a/timesheetweek_document.php +++ b/timesheetweek_document.php @@ -142,8 +142,8 @@ // Set $enablepermissioncheck to 1 to enable a minimum low level of checks $enablepermissioncheck = getDolGlobalInt('TIMESHEETWEEK_ENABLE_PERMISSION_CHECK'); if ($enablepermissioncheck) { - $permissiontoread = $user->hasRight('timesheetweek', 'timesheetweek', 'read'); - $permissiontoadd = $user->hasRight('timesheetweek', 'timesheetweek', 'write'); // Used by the include of actions_addupdatedelete.inc.php and actions_linkedfiles.inc.php + $permissiontoread = $user->hasRight('timesheetweek', 'read'); + $permissiontoadd = $user->hasRight('timesheetweek', 'write'); // Used by the include of actions_addupdatedelete.inc.php and actions_linkedfiles.inc.php } else { $permissiontoread = 1; $permissiontoadd = 1; diff --git a/timesheetweek_list.php b/timesheetweek_list.php index 7d16e51..560b1b3 100644 --- a/timesheetweek_list.php +++ b/timesheetweek_list.php @@ -23,19 +23,25 @@ // EN: Check permissions before loading any additional resources to abort early. // FR: Vérifie les permissions avant de charger d'autres ressources pour interrompre immédiatement. -$permRead = $user->hasRight('timesheetweek','timesheetweek','read'); -$permReadChild = $user->hasRight('timesheetweek','timesheetweek','readChild'); -$permReadAll = $user->hasRight('timesheetweek','timesheetweek','readAll'); -$permWrite = $user->hasRight('timesheetweek','timesheetweek','write'); -$permWriteChild = $user->hasRight('timesheetweek','timesheetweek','writeChild'); -$permWriteAll = $user->hasRight('timesheetweek','timesheetweek','writeAll'); -$permDelete = $user->hasRight('timesheetweek','timesheetweek','delete'); -$permDeleteChild = $user->hasRight('timesheetweek','timesheetweek','deleteChild'); -$permDeleteAll = $user->hasRight('timesheetweek','timesheetweek','deleteAll'); -$permValidate = $user->hasRight('timesheetweek','timesheetweek','validate'); -$permValidateOwn = $user->hasRight('timesheetweek','timesheetweek','validateOwn'); -$permValidateChild = $user->hasRight('timesheetweek','timesheetweek','validateChild'); -$permValidateAll = $user->hasRight('timesheetweek','timesheetweek','validateAll'); +$permRead = $user->hasRight('timesheetweek', 'read'); +$permReadChild = $user->hasRight('timesheetweek','readChild'); +$permReadAll = $user->hasRight('timesheetweek','readAll'); +$permWrite = $user->hasRight('timesheetweek','write'); +$permWriteChild = $user->hasRight('timesheetweek','writeChild'); +$permWriteAll = $user->hasRight('timesheetweek','writeAll'); +$permDelete = $user->hasRight('timesheetweek','delete'); +$permDeleteChild = $user->hasRight('timesheetweek','deleteChild'); +$permDeleteAll = $user->hasRight('timesheetweek','deleteAll'); +$permSeal = $user->hasRight('timesheetweek','seal'); +$permValidate = $user->hasRight('timesheetweek','validate'); +$permValidateOwn = $user->hasRight('timesheetweek','validateOwn'); +$permValidateChild = $user->hasRight('timesheetweek','validateChild'); +$permValidateAll = $user->hasRight('timesheetweek','validateAll'); +// EN: Prepare Dolibarr's generic permission flags for mass-action helpers. +// FR: Prépare les indicateurs de permission Dolibarr pour les helpers d'actions de masse. +$permissiontoread = ($permRead || $permReadChild || $permReadAll); +$permissiontoadd = ($permWrite || $permWriteChild || $permWriteAll); +$permissiontodelete = ($permDelete || $permDeleteChild || $permDeleteAll || !empty($user->admin)); $canSeeAllEmployees = (!empty($user->admin) || $permReadAll || $permWriteAll || $permDeleteAll || $permValidateAll); $permViewAny = ($permRead || $permReadChild || $permReadAll || $permWrite || $permWriteChild || $permWriteAll || $permDelete || $permDeleteChild || $permDeleteAll || $permValidate || $permValidateOwn || $permValidateChild || $permValidateAll || !empty($user->admin)); @@ -70,7 +76,77 @@ // FR: Détecte si le module Multicompany est activé pour exposer les données spécifiques d'entité. $multicompanyEnabled = !empty($conf->multicompany->enabled); +if (!function_exists('tw_can_validate_timesheet_masslist')) { + /** + * EN: Determine if the current user is allowed to validate the provided sheet. + * FR: Détermine si l'utilisateur courant est autorisé à valider la feuille fournie. + * + * @param TimesheetWeek $sheet Sheet to evaluate + * @param User $user Current Dolibarr user + * @param bool $permValidate Direct validation right + * @param bool $permValidateOwn Validation on own sheets + * @param bool $permValidateChild Validation on subordinate sheets + * @param bool $permValidateAll Global validation right + * @param bool $permWrite Write right on own sheets + * @param bool $permWriteChild Write right on subordinate sheets + * @param bool $permWriteAll Global write right + * @return bool True when validation is authorised + */ + function tw_can_validate_timesheet_masslist( + TimesheetWeek $sheet, + User $user, + $permValidate, + $permValidateOwn, + $permValidateChild, + $permValidateAll, + $permWrite, + $permWriteChild, + $permWriteAll + ) { + // EN: Check explicit validation rights first to keep the behaviour consistent with the card view. + // FR: Vérifie d'abord les droits explicites de validation pour rester cohérent avec la fiche détaillée. + $hasExplicitValidation = ($permValidate || $permValidateOwn || $permValidateChild || $permValidateAll); + if (!empty($user->admin)) { + $permValidateAll = true; + $hasExplicitValidation = true; + } + + if (!$hasExplicitValidation) { + // EN: Reuse write permissions when legacy configurations rely on them for validation. + // FR: Réutilise les permissions d'écriture lorsque les anciennes configurations s'en servent pour valider. + if ($permWriteAll) { + $permValidateAll = true; + } + if ($permWriteChild) { + $permValidateChild = true; + } + if ($permWrite || $permWriteChild || $permWriteAll) { + if ((int) $sheet->fk_user_valid === (int) $user->id) { + $permValidate = true; + } + if (!$permValidateChild && $permWriteChild) { + $permValidateChild = true; + } + } + } + + if ($permValidateAll) { + return true; + } + if ($permValidateChild && tw_is_manager_of($sheet->fk_user, $user)) { + return true; + } + if ($permValidateOwn && ((int) $user->id === (int) $sheet->fk_user)) { + return true; + } + if ($permValidate && ((int) $user->id === (int) $sheet->fk_user_valid)) { + return true; + } + + return false; + } +} /** * Params */ @@ -79,7 +155,7 @@ $show_files = GETPOSTINT('show_files'); $confirm = GETPOST('confirm', 'alpha'); $cancel = GETPOST('cancel', 'alpha'); -$toselect = GETPOST('toselect', 'array'); +$toselect = GETPOST('toselect', 'array', 2); $contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'timesheetweeklist'; $sortfield = GETPOST('sortfield', 'aZ09comma'); @@ -239,17 +315,297 @@ include DOL_DOCUMENT_ROOT.'/core/actions_changeselectedfields.inc.php'; /** - * Mass actions (UI) - */ -$arrayofmassactions = array( - 'approve_selection' => img_picto('', 'validate', 'class="pictofixedwidth"').$langs->trans("ApproveSelection"), - 'refuse_selection' => img_picto('', 'warning', 'class="pictofixedwidth"').$langs->trans("RefuseSelection"), - 'predelete' => img_picto('', 'delete', 'class="pictofixedwidth"').$langs->trans("DeleteSelection"), +* Mass actions (UI) +*/ +$arrayofmassactions = array(); +// EN: Offer approval when the user holds validation or equivalent legacy permissions. +// FR: Propose l'approbation lorsque l'utilisateur dispose des droits de validation ou équivalents hérités. +$canDisplayValidationActions = ( + $permValidate || $permValidateOwn || $permValidateChild || $permValidateAll || + $permWrite || $permWriteChild || $permWriteAll || !empty($user->admin) ); -$massactionbutton = $form->selectMassAction('', $arrayofmassactions); +if ($canDisplayValidationActions) { + $arrayofmassactions['approve_selection'] = img_picto('', 'check', 'class="pictofixedwidth"').$langs->trans('ApproveSelection'); + $arrayofmassactions['refuse_selection'] = img_picto('', 'uncheck', 'class="pictofixedwidth"').$langs->trans('RefuseSelection'); +} +// EN: Display the sealing control only to users granted with the dedicated right. +// FR: Affiche le contrôle de scellement uniquement pour les utilisateurs disposant du droit dédié. +if ($permSeal) { + $arrayofmassactions['sceller'] = img_picto('', 'lock', 'class="pictofixedwidth"').$langs->trans('SealSelection'); +} +// EN: Allow PDF summary generation to any user allowed to read the listed sheets. +// FR: Autorise la génération d'un PDF de synthèse à tout utilisateur habilité à lire les feuilles listées. +if ($permissiontoread) { + $arrayofmassactions['generate_summary_pdf'] = img_picto('', 'pdf', 'class="pictofixedwidth"').$langs->trans('GenerateSummaryPdf'); +} +// EN: Expose the draft-only bulk deletion with Dolibarr's confirmation flow when the operator may delete sheets. +// FR: Expose la suppression massive limitée aux brouillons avec la confirmation Dolibarr lorsque l'opérateur peut supprimer des feuilles. +if ($permissiontodelete) { + $arrayofmassactions['predelete'] = img_picto('', 'delete', 'class="pictofixedwidth"').$langs->trans('DeleteSelection'); +} + +$massactionbutton = $form->selectMassAction($massaction, $arrayofmassactions); +$objectclass = 'TimesheetWeek'; +$objectlabel = 'TimesheetWeek'; +$object = new TimesheetWeek($db); + +$uploaddir = !empty($conf->timesheetweek->multidir_output[$conf->entity] ?? null) +? $conf->timesheetweek->multidir_output[$conf->entity] +: (!empty($conf->timesheetweek->dir_output) ? $conf->timesheetweek->dir_output : DOL_DATA_ROOT.'/timesheetweek'); +$upload_dir = $uploaddir; +// Affiche le menu d’actions +$showmassactionbutton = 1; + +include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php'; + +// EN: Normalise the selected identifiers provided by Dolibarr's mass-action handler. +// FR: Normalise les identifiants sélectionnés fournis par le gestionnaire d'actions de masse de Dolibarr. $arrayofselected = is_array($toselect) ? $toselect : array(); +$massActionProcessed = false; + +if ($massaction === 'approve_selection') { + $massActionProcessed = true; + if (!$canDisplayValidationActions) { + // EN: Stop the approval when the operator lacks validation permissions. + // FR: Empêche l'approbation lorsque l'opérateur n'a pas les permissions de validation. + setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors'); + } else { + $db->begin(); + $ok = 0; + $ko = array(); + foreach ((array) $arrayofselected as $id) { + $id = (int) $id; + if ($id <= 0) { + continue; + } + $o = new TimesheetWeek($db); + if ($o->fetch($id) <= 0) { + $ko[] = '#'.$id; + continue; + } + if (!tw_can_validate_timesheet_masslist($o, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) { + // EN: Reject the sheet when the current user cannot validate it according to delegation rules. + // FR: Rejette la feuille lorsque l'utilisateur courant ne peut pas la valider selon les règles de délégation. + $ko[] = $o->ref ?: '#'.$id; + continue; + } + $res = $o->approve($user); + if ($res > 0) { + $ok++; + } else { + $ko[] = $o->ref ?: '#'.$id; + } + } + if ($ko) { + $db->rollback(); + } else { + $db->commit(); + } + if ($ok) { + setEventMessages($langs->trans('TimesheetWeekMassApproveSuccess', $ok), null, 'mesgs'); + } + if ($ko) { + setEventMessages($langs->trans('TimesheetWeekMassActionErrors', implode(', ', $ko)), null, 'errors'); + } + } +} + +if ($massaction === 'refuse_selection') { + $massActionProcessed = true; + if (!$canDisplayValidationActions) { + // EN: Prevent the refusal when the operator is not authorised to validate sheets. + // FR: Empêche le refus lorsque l'opérateur n'est pas autorisé à valider les feuilles. + setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors'); + } else { + $db->begin(); + $ok = 0; + $ko = array(); + foreach ((array) $arrayofselected as $id) { + $id = (int) $id; + if ($id <= 0) { + continue; + } + $o = new TimesheetWeek($db); + if ($o->fetch($id) <= 0) { + $ko[] = '#'.$id; + continue; + } + if (!tw_can_validate_timesheet_masslist($o, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) { + // EN: Skip the refusal when the user cannot manage the employee under current rights. + // FR: Ignore le refus lorsque l'utilisateur ne peut pas gérer l'employé avec les droits actuels. + $ko[] = $o->ref ?: '#'.$id; + continue; + } + $res = $o->refuse($user); + if ($res > 0) { + $ok++; + } else { + $ko[] = $o->ref ?: '#'.$id; + } + } + if ($ko) { + $db->rollback(); + } else { + $db->commit(); + } + if ($ok) { + setEventMessages($langs->trans('TimesheetWeekMassRefuseSuccess', $ok), null, 'mesgs'); + } + if ($ko) { + setEventMessages($langs->trans('TimesheetWeekMassActionErrors', implode(', ', $ko)), null, 'errors'); + } + } +} + +if ($massaction === 'sceller') { + $massActionProcessed = true; + if (!$permSeal) { + // EN: Refuse sealing when the operator does not own the dedicated right. + // FR: Refuse le scellement lorsque l'opérateur ne possède pas le droit dédié. + setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors'); + } else { + $db->begin(); + $ok = 0; + $ko = array(); + foreach ((array) $arrayofselected as $id) { + $id = (int) $id; + if ($id <= 0) { + continue; + } + $o = new TimesheetWeek($db); + if ($o->fetch($id) <= 0) { + $ko[] = '#'.$id; + continue; + } + if (!tw_can_validate_timesheet_masslist($o, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) { + // EN: Keep the sheet untouched when the manager cannot act on the employee scope. + // FR: Laisse la feuille inchangée lorsque le gestionnaire ne peut pas agir sur le périmètre de l'employé. + $ko[] = $o->ref ?: '#'.$id; + continue; + } + $res = $o->seal($user); + if ($res > 0) { + $ok++; + } else { + $ko[] = $o->ref ?: '#'.$id; + } + } + if ($ko) { + $db->rollback(); + } else { + $db->commit(); + } + if ($ok) { + setEventMessages($langs->trans('TimesheetWeekMassSealSuccess', $ok), null, 'mesgs'); + } + if ($ko) { + setEventMessages($langs->trans('TimesheetWeekMassActionErrors', implode(', ', $ko)), null, 'errors'); + } + } +} +if ($massaction === 'generate_summary_pdf') { + $massActionProcessed = true; + if (!$permissiontoread) { + // EN: Block summary export when the user has no read permission. + // FR: Bloque l'export de synthèse lorsque l'utilisateur n'a pas le droit de lecture. + setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors'); + } else { + if (empty($arrayofselected)) { + // EN: Notify operators that a selection is required before generating the summary. + // FR: Informe les opérateurs qu'une sélection est nécessaire avant de générer la synthèse. + setEventMessages($langs->trans('TimesheetWeekErrorNoSelection'), null, 'errors'); + } else { + dol_include_once('/timesheetweek/lib/timesheetweek_pdf.lib.php'); + $result = tw_generate_summary_pdf($db, $conf, $langs, $user, $arrayofselected, $permRead, $permReadChild, $permReadAll); + if (empty($result['success'])) { + // EN: Report generation issues to the operator. + // FR: Signale les problèmes de génération à l'opérateur. + if (!empty($result['errors'])) { + setEventMessages(null, $result['errors'], 'errors'); + } else { + setEventMessages($langs->trans('ErrorUnknown'), null, 'errors'); + } + } else { + if (!empty($result['warnings'])) { + setEventMessages(null, $result['warnings'], 'warnings'); + } + if (!empty($result['relative'])) { + $downloadUrl = DOL_URL_ROOT.'/document.php?modulepart=timesheetweek&file='.urlencode($result['relative']).'&entity='.$conf->entity.'&permission=read'; + header('Location: '.$downloadUrl); + exit; + } + setEventMessages($langs->trans('TimesheetWeekSummaryGenerated'), null, 'mesgs'); + } + } + } +} +if ($massaction === 'delete') { + $massActionProcessed = true; + if (!$permissiontodelete) { + // EN: Block the deletion when the user lacks the necessary rights. + // FR: Bloque la suppression lorsque l'utilisateur ne dispose pas des droits nécessaires. + setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors'); + } else { + $db->begin(); + $ok = 0; + $ko = array(); + $nonDraftDetected = false; + foreach ((array) $arrayofselected as $id) { + $id = (int) $id; + if ($id <= 0) { + continue; + } + $o = new TimesheetWeek($db); + if ($o->fetch($id) <= 0) { + $ko[] = '#'.$id; + continue; + } + if (!tw_can_act_on_user($o->fk_user, $permDelete, $permDeleteChild, ($permDeleteAll || !empty($user->admin)), $user)) { + // EN: Prevent deletion outside the managerial scope defined by Dolibarr rights. + // FR: Empêche la suppression en dehors du périmètre managérial défini par les droits Dolibarr. + $ko[] = $o->ref ?: '#'.$id; + continue; + } + if ((int) $o->status !== TimesheetWeek::STATUS_DRAFT) { + // EN: Enforce the draft-only restriction required for bulk deletions. + // FR: Applique la restriction aux brouillons exigée pour les suppressions massives. + $ko[] = $o->ref ?: '#'.$id; + $nonDraftDetected = true; + continue; + } + $res = $o->delete($user); + if ($res > 0) { + $ok++; + } else { + $ko[] = ($o->ref ?: '#'.$id); + } + } + if ($ko) { + $db->rollback(); + } else { + $db->commit(); + } + if ($ok) { + setEventMessages($langs->trans('RecordsDeleted', $ok), null, 'mesgs'); + } + if ($ko) { + setEventMessages($langs->trans('TimesheetWeekMassActionErrors', implode(', ', $ko)), null, 'errors'); + } + if ($nonDraftDetected) { + // EN: Inform the operator that only draft sheets are eligible for removal. + // FR: Informe l'opérateur que seules les feuilles en brouillon sont éligibles à la suppression. + setEventMessages($langs->trans('TimesheetWeekMassDeleteOnlyDraft'), null, 'errors'); + } + } + $massaction = ''; +} + +if ($massActionProcessed) { + $massaction = ''; +} + if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x', 'alpha') || GETPOST('button_removefilter', 'alpha')) { $search_ref = ''; $search_user = 0; @@ -414,9 +770,7 @@ // FR: Conserve la limite sélectionnée dans les liens de pagination pour respecter le choix de l'utilisateur. $param .= '&limit='.(int) $limit; -$newcardbutton = dolGetButtonTitle($langs->trans('New'), '', 'fa fa-plus-circle', dol_buildpath('/timesheetweek/timesheetweek_card.php', 1).'?action=create', '', $user->hasRight('timesheetweek','timesheetweek','write')); - -print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, $massactionbutton, $num, $nbtotalofrecords, 'bookcal', 0, $newcardbutton, '', $limit, 0, 0, 1); +$newcardbutton = dolGetButtonTitle($langs->trans('New'), '', 'fa fa-plus-circle', dol_buildpath('/timesheetweek/timesheetweek_card.php', 1).'?action=create', '', $user->hasRight('timesheetweek','write')); /** * Column selector on left of titles @@ -440,6 +794,16 @@ // FR: Conserve la limite de liste sélectionnée lors des filtrages tout en évitant les identifiants dupliqués dans le DOM. print ''; +print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, $massactionbutton, $num, $nbtotalofrecords, 'bookcal', 0, $newcardbutton, '', $limit, 0, 0, 1); + +$topicmail = "SendTimesheetWeekRef"; +$modelmail = "timesheetweek"; +$objecttmp = new TimesheetWeek($db); +$trackid = 'tsw'.$object->id; +// EN: Display Dolibarr's standard confirmation prompts for mass actions. +// FR: Affiche les fenêtres de confirmation standard de Dolibarr pour les actions de masse. +include DOL_DOCUMENT_ROOT.'/core/tpl/massactions_pre.tpl.php'; + print '
'; print ''."\n"; diff --git a/timesheetweek_note.php b/timesheetweek_note.php index e8637a4..22545db 100644 --- a/timesheetweek_note.php +++ b/timesheetweek_note.php @@ -120,9 +120,9 @@ // Set $enablepermissioncheck to 1 to enable a minimum low level of checks $enablepermissioncheck = getDolGlobalInt('TIMESHEETWEEK_ENABLE_PERMISSION_CHECK'); if ($enablepermissioncheck) { - $permissiontoread = $user->hasRight('timesheetweek', 'timesheetweek', 'read'); - $permissiontoadd = $user->hasRight('timesheetweek', 'timesheetweek', 'write'); - $permissionnote = $user->hasRight('timesheetweek', 'timesheetweek', 'write'); // Used by the include of actions_setnotes.inc.php + $permissiontoread = $user->hasRight('timesheetweek', 'read'); + $permissiontoadd = $user->hasRight('timesheetweek', 'write'); + $permissionnote = $user->hasRight('timesheetweek', 'write'); // Used by the include of actions_setnotes.inc.php } else { $permissiontoread = 1; $permissiontoadd = 1;