Skip to content
7 changes: 7 additions & 0 deletions htdocs/core/modules/modHoliday.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,13 @@ public function __construct($db)
*/
public function init($options = '')
{
// EN: Ensure holiday tables are created or updated during module activation.
// FR: Garantir la création ou la mise à jour des tables des congés lors de l'activation du module.
$result = $this->_load_tables('/install/mysql/', 'holiday');
if ($result < 0) {
return -1;
}

// Permissions
$this->remove($options);

Expand Down
188 changes: 163 additions & 25 deletions htdocs/holiday/card.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Copyright (C) 2024 Charlene Benke <[email protected]>
* Copyright (C) 2025 MDW <[email protected]>
*
* Copyright (C) 2025 Pierre Ardoin <[email protected]>
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, orwrite
Expand Down Expand Up @@ -43,6 +44,65 @@
require_once DOL_DOCUMENT_ROOT.'/core/lib/holiday.lib.php';
require_once DOL_DOCUMENT_ROOT.'/holiday/class/holiday.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';

/**
* Determine the default second approver based on the hierarchy of the first approver.
* Détermine le second valideur par défaut en fonction de la hiérarchie du premier valideur.
*
* @param DoliDB $db Database handler / Gestionnaire de base de données
* @param int $firstApproverId First approver identifier / Identifiant du premier valideur
* @param array<int,int> $allowedApprovers Optional allowed approvers / Liste optionnelle des valideurs autorisés
* @return int Identifier of the second approver or 0 / Identifiant du second valideur ou 0
*/
function holiday_get_default_second_approver(DoliDB $db, $firstApproverId, array $allowedApprovers = array())
{
// Secure the incoming identifier / Sécurise l'identifiant en entrée
$firstApproverId = (int) $firstApproverId;
if ($firstApproverId <= 0) {
return 0;
}

// Load the selected first approver / Charge le premier valideur sélectionné
$firstApprover = new User($db);
if ($firstApprover->fetch($firstApproverId) <= 0) {
return 0;
}

// Try the forced approver stored in llx_user.fk_user_approve2 / Tente le valideur forcé stocké dans llx_user.fk_user_approve2
$forcedSecondApproverId = 0;
$sql = 'SELECT fk_user_approve2 FROM '.MAIN_DB_PREFIX."user WHERE rowid = ".$firstApproverId;
$resql = $db->query($sql);
if ($resql) {
$obj = $db->fetch_object($resql);
if ($obj) {
$forcedSecondApproverId = (int) $obj->fk_user_approve2;
}
$db->free($resql);
}
// Fallback on dedicated holiday value / Replie sur la valeur dédiée aux congés
if ($forcedSecondApproverId <= 0) {
$forcedSecondApproverId = (int) $firstApprover->fk_user_holiday_validator2;
}
if ($forcedSecondApproverId > 0) {
if (empty($allowedApprovers) || in_array($forcedSecondApproverId, $allowedApprovers, true)) {
return $forcedSecondApproverId;
}
}

// Extract the manager identifier from the hierarchy / Récupère l'identifiant du responsable hiérarchique

Check failure on line 93 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible

Check failure on line 93 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible
$managerId = (int) $firstApprover->fk_user;
if ($managerId <= 0) {
return 0;
}

// Check if the manager can validate if a list is provided / Vérifie que le responsable peut valider si une liste est fournie

Check failure on line 99 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible

Check failure on line 99 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible
if (!empty($allowedApprovers) && !in_array($managerId, $allowedApprovers, true)) {
return 0;
}

return $managerId;
}

/**
* @var Conf $conf
Expand Down Expand Up @@ -280,9 +340,16 @@
setEventMessages($langs->transnoentitiesnoconv('InvalidValidator'), null, 'errors');
$error++;
}
// Allow selection of the second approver
// Allow selection of the second approver / Gère la sélection du second valideur
if (getDolGlobalString('HOLIDAY_REQUIRE_DOUBLE_APPROVAL')) {
// Validate the chosen second approver
// Auto select the hierarchical manager if missing / Sélectionne automatiquement le responsable hiérarchique si absent

Check failure on line 345 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible

Check failure on line 345 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible
if ($approverid2 < 1) {
$defaultSecondApprover = holiday_get_default_second_approver($db, $approverid, $approverslist);
if ($defaultSecondApprover > 0) {
$approverid2 = $defaultSecondApprover;
}
}
// Validate the chosen second approver / Valide le second valideur choisi
if ($approverid2 < 1) {
setEventMessages($langs->trans('SecondApproverRequired'), null, 'errors');
$error++;
Expand All @@ -305,9 +372,9 @@
$object->fk_user = $fuserid;
$object->description = $description;
$object->fk_validator = $approverid;
// Offer edition of the second approver
if (getDolGlobalString('HOLIDAY_REQUIRE_DOUBLE_APPROVAL')) {
// Keep the expected second approver
// Offer edition of the second approver / Permet l'édition du second valideur
if (getDolGlobalString('HOLIDAY_REQUIRE_DOUBLE_APPROVAL')) {
// Keep the expected second approver / Conserve le second valideur prévu
$object->fk_user_approve2 = ($approverid2 > 0 ? $approverid2 : 0);
} else {
$object->fk_user_approve2 = 0;
Expand Down Expand Up @@ -342,17 +409,25 @@

$object->oldcopy = dol_clone($object, 2); // @phan-suppress-current-line PhanTypeMismatchProperty

$object->fk_validator = GETPOSTINT('valideur');
// Capture the second approver during quick edit
$approverid = GETPOSTINT('valideur');
$object->fk_validator = $approverid;
// Capture the second approver during quick edit / Récupère le second valideur lors de l'édition rapide
$approverid2 = GETPOSTINT('valideur2');
$localerror = 0;
if (getDolGlobalString('HOLIDAY_REQUIRE_DOUBLE_APPROVAL')) {
// Validate and store the second approver during quick edit
$approverslist = $object->fetch_users_approver_holiday();
// Auto select the hierarchical manager if missing / Sélectionne automatiquement le responsable hiérarchique si absent

Check failure on line 419 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible

Check failure on line 419 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible
if ($approverid2 < 1) {
$defaultSecondApprover = holiday_get_default_second_approver($db, $approverid, $approverslist);
if ($defaultSecondApprover > 0) {
$approverid2 = $defaultSecondApprover;
}
}
// Validate and store the second approver during quick edit / Valide et sauvegarde le second valideur lors de l'édition rapide
if ($approverid2 < 1) {
setEventMessages($langs->trans('SecondApproverRequired'), null, 'warnings');
$localerror++;
}
$approverslist = $object->fetch_users_approver_holiday();
if (!empty($approverslist) && !in_array($approverid2, $approverslist)) {
setEventMessages($langs->trans('InvalidSecondValidatorCP'), null, 'warnings');
$localerror++;
Expand Down Expand Up @@ -444,19 +519,28 @@
$action = 'edit';
}
if (getDolGlobalString('HOLIDAY_REQUIRE_DOUBLE_APPROVAL')) {
// Validate the second approver while editing
if ($approverid2 < 1) {
setEventMessages($langs->trans('SecondApproverRequired'), null, 'warnings');
$error++;
$action = 'edit';
}
$approverslist = $object->fetch_users_approver_holiday();
if (!empty($approverslist) && !in_array($approverid2, $approverslist)) {
setEventMessages($langs->trans('InvalidSecondValidatorCP'), null, 'warnings');
$error++;
$action = 'edit';
$approverslist = $object->fetch_users_approver_holiday();

Check failure on line 522 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected at least 5 tabs, found 4
// Auto select the hierarchical manager if missing / Sélectionne automatiquement le responsable hiérarchique si absent

Check failure on line 523 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible

Check failure on line 523 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected at least 5 tabs, found 4

Check failure on line 523 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible
if ($approverid2 < 1) {

Check failure on line 524 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 5 tabs, found 4
$defaultSecondApprover = holiday_get_default_second_approver($db, $approverid, $approverslist);
if ($defaultSecondApprover > 0) {
$approverid2 = $defaultSecondApprover;
}
}
// Validate the second approver while editing / Valide le second valideur pendant l'édition
if ($approverid2 < 1) {
setEventMessages($langs->trans('SecondApproverRequired'), null, 'warnings');
$error++;
$action = 'edit';
}
if (!empty($approverslist) && !in_array($approverid2, $approverslist)) {
setEventMessages($langs->trans('InvalidSecondValidatorCP'), null, 'warnings');
$error++;
$action = 'edit';
}
}



// If there is no Business Days within request
$nbopenedday = num_open_day($date_debut_gmt, $date_fin_gmt, 0, 1, $halfday);
Expand Down Expand Up @@ -1301,12 +1385,60 @@
if (empty($include_users)) {
print img_warning().' '.$langs->trans("NobodyHasPermissionToValidateHolidays");
} else {
// Build a reusable mapping of first approvers to their manager / Construit un mapping réutilisable entre le premier valideur et son responsable

Check failure on line 1388 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible

Check failure on line 1388 in htdocs/holiday/card.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

responsable ==> responsible
$defaultSecondApproversMap = array();
foreach ($include_users as $potentialFirstApproverId) {
$possibleSecondApproverId = holiday_get_default_second_approver($db, $potentialFirstApproverId, $include_users);
if ($possibleSecondApproverId > 0) {
$defaultSecondApproversMap[$potentialFirstApproverId] = $possibleSecondApproverId;
}
}

$defaultselectuser2 = GETPOSTINT('valideur2');
if (empty($defaultselectuser2) && !empty($defaultselectuser)) {
$defaultselectuser2 = $defaultselectuser;
$defaultFromHierarchy = (isset($defaultSecondApproversMap[$defaultselectuser]) ? $defaultSecondApproversMap[$defaultselectuser] : holiday_get_default_second_approver($db, $defaultselectuser, $include_users));
if ($defaultFromHierarchy > 0) {
$defaultselectuser2 = $defaultFromHierarchy;
} else {
$defaultselectuser2 = $defaultselectuser;
}
}
$s2 = $form->select_dolusers($defaultselectuser2, "valideur2", 1, '', 0, $include_users, '', '0,'.$conf->entity, 0, 0, '', 0, '', 'minwidth200 maxwidth500');
print img_picto('', 'user', 'class="pictofixedwidth"').$form->textwithpicto($s2, $langs->trans("AnyOtherInThisListCanValidate"));
if ($action == 'create' && !empty($defaultSecondApproversMap)) {
$defaultSecondApproversJson = json_encode($defaultSecondApproversMap);
print '<script>';
print 'jQuery(function($){';
print '\\t// Auto fill second approver from hierarchy / Préremplit le second valideur via la hiérarchie';
print '\\tvar defaults = '.(!empty($defaultSecondApproversJson) ? $defaultSecondApproversJson : '{}').';';
print '\\tvar allowAutoUpdate = '.(GETPOSTINT('valideur2') > 0 ? 'false' : 'true').';';
print '\tvar $first = $("#valideur");';
print '\tvar $second = $("#valideur2");';
print '\tfunction applyDefaultSecond(){';
print '\t\tif (!allowAutoUpdate) { return; }';
print '\t\tvar selected = $first.val();';
print '\t\tif (defaults[selected]) {';
print '\t\t\t$second.val(defaults[selected]);';
print '\t\t\t$second.data("autofill", 1);';
print '\t\t}';
print '\t}';
print '\tif (allowAutoUpdate) {';
print '\t\tapplyDefaultSecond();';
print '\t}';
print '\t$first.on("change", function(){';
print '\t\t// Refresh default second approver when first approver changes / Rafraîchit le second valideur par défaut lorsque le premier change';
print '\t\tapplyDefaultSecond();';
print '\t});';
print '\t$second.on("change", function(){';
print '\t\t// Stop auto update after manual choice / Arrête l\'auto mise à jour après un choix manuel';
print '\t\tif ($second.data("autofill") !== 1) {';
print '\t\t\tallowAutoUpdate = false;';
print '\t\t}';
print '\t\t$second.data("autofill", 0);';
print '\t});';
print '});';
print '</script>';
}
}
print '</td>';
print '</tr>';
Expand Down Expand Up @@ -1617,12 +1749,18 @@
if (!in_array($object->fk_user_approve2, $include_users)) {
$include_users[] = $object->fk_user_approve2;
}
// Fetch the proposed second approver
// Fetch the proposed second approver / Récupère le second valideur proposé
$defaultselectuser2 = $object->fk_user_approve2;
// Preserve the posted value for the second approver
if (empty($defaultselectuser2) && !empty($object->fk_validator)) {
$defaultFromHierarchy = holiday_get_default_second_approver($db, $object->fk_validator, $include_users);
if ($defaultFromHierarchy > 0) {
$defaultselectuser2 = $defaultFromHierarchy;
}
}
// Preserve the posted value for the second approver / Préserve la valeur postée pour le second valideur
if ($action == 'editvalidator' && GETPOSTINT('valideur2') > 0) {
// Refresh the second approver with the posted value
$defaultselectuser2 = GETPOSTINT('valideur2');
// Refresh the second approver with the posted value / Rafraîchit le second valideur avec la valeur postée
$defaultselectuser2 = GETPOSTINT('valideur2');
}
$s2 = $form->select_dolusers($defaultselectuser2, "valideur2", (($action == 'editvalidator') ? 0 : 1), $arrayofvalidatorstoexclude, 0, $include_users);
print '<div class="paddingleft">'.$form->textwithpicto($s2, $langs->trans("AnyOtherInThisListCanValidate")).'</div>';
Expand Down
1 change: 1 addition & 0 deletions htdocs/holiday/class/holiday.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Copyright (C) 2016 Juanjo Menent <[email protected]>
* Copyright (C) 2018-2025 Frédéric France <[email protected]>
* Copyright (C) 2024-2025 MDW <[email protected]>
* Copyright (C) 2025 Pierre Ardoin <[email protected]>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down
8 changes: 8 additions & 0 deletions htdocs/install/mysql/tables/llx_holiday-holiday.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- EN: Ensure double approval columns exist for holiday module activation / FR: Garantit l'existence des colonnes de double approbation lors de l'activation du module Congés
ALTER TABLE llx_holiday
ADD COLUMN date_approval2 DATETIME DEFAULT NULL AFTER fk_user_approve,
ADD COLUMN fk_user_approve2 integer DEFAULT NULL AFTER date_approval2;

-- EN: Ensure forced second holiday approver column exists / FR: Garantit la présence du second valideur de congés forcé
ALTER TABLE llx_user
ADD COLUMN fk_user_holiday_validator2 integer DEFAULT NULL AFTER fk_user_holiday_validator;
67 changes: 35 additions & 32 deletions htdocs/install/mysql/tables/llx_holiday.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,40 @@

CREATE TABLE llx_holiday
(
rowid integer NOT NULL AUTO_INCREMENT PRIMARY KEY,
ref varchar(30) NOT NULL,
ref_ext varchar(255),
entity integer DEFAULT 1 NOT NULL, -- Multi company id
fk_user integer NOT NULL,
fk_user_create integer,
fk_user_modif integer,
fk_type integer NOT NULL,
date_create DATETIME NOT NULL,
description VARCHAR( 255 ) NOT NULL,
date_debut DATE NOT NULL,
date_fin DATE NOT NULL,
halfday integer DEFAULT 0, -- 0=start morning and end afternoon, -1=start afternoon end afternoon, 1=start morning and end morning, 2=start afternoon and end morning
nb_open_day double(24,8) DEFAULT NULL, -- DENORMALIZED FIELD. number of open days of holiday. Not always set. More reliable when re-calculated with num_open_days(date_debut, date_fin, halfday).
statut integer NOT NULL DEFAULT 1, -- status of leave request
fk_validator integer NOT NULL, -- who should approve the leave
date_valid DATETIME DEFAULT NULL, -- date validation
fk_user_valid integer DEFAULT NULL, -- user validation
date_approval DATETIME DEFAULT NULL, -- date approval
fk_user_approve integer DEFAULT NULL, -- user approval
date_approval2 DATETIME DEFAULT NULL, -- EN: date of second approval / FR: date de la seconde approbation
fk_user_approve2 integer DEFAULT NULL, -- EN: user for second approval / FR: utilisateur pour la seconde approbation
date_refuse DATETIME DEFAULT NULL,
fk_user_refuse integer DEFAULT NULL,
date_cancel DATETIME DEFAULT NULL,
fk_user_cancel integer DEFAULT NULL,
detail_refuse varchar( 250 ) DEFAULT NULL,
note_private text,
note_public text,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
import_key varchar(14),
extraparams varchar(255) -- for other parameters with json format
rowid integer NOT NULL AUTO_INCREMENT PRIMARY KEY,
ref varchar(30) NOT NULL,
ref_ext varchar(255),
entity integer DEFAULT 1 NOT NULL, -- EN: Multi company id / FR: Identifiant multi-société
fk_user integer NOT NULL,
fk_user_create integer,
fk_user_modif integer,
fk_type integer NOT NULL,
date_create DATETIME NOT NULL,
description VARCHAR(255) NOT NULL,
date_debut DATE NOT NULL,
date_fin DATE NOT NULL,
halfday integer DEFAULT 0, -- EN: 0=start morning and end afternoon, -1=start afternoon end afternoon, 1=start morning and end morning, 2=start afternoon and end morning / FR: 0=début matin fin après-midi, -1=début après-midi fin après-midi, 1=début matin fin matin, 2=début après-midi fin matin
nb_open_day double(24,8) DEFAULT NULL, -- EN: DENORMALIZED FIELD. number of open days of holiday. Not always set. More reliable when re-calculated with num_open_days(date_debut, date_fin, halfday). / FR: CHAMP DÉNORMALISÉ. Nombre de jours ouvrés de congés. Pas toujours défini. Plus fiable lors d'un recalcul avec num_open_days(date_debut, date_fin, halfday).
statut integer NOT NULL DEFAULT 1, -- EN: status of leave request / FR: statut de la demande de congé
fk_validator integer NOT NULL, -- EN: who should approve the leave / FR: personne devant approuver le congé
date_valid DATETIME DEFAULT NULL, -- EN: date validation / FR: date de validation
fk_user_valid integer DEFAULT NULL, -- EN: user validation / FR: utilisateur de validation
date_approval DATETIME DEFAULT NULL, -- EN: date approval / FR: date d'approbation
fk_user_approve integer DEFAULT NULL, -- EN: user approval / FR: utilisateur approbateur
date_refuse DATETIME DEFAULT NULL,
fk_user_refuse integer DEFAULT NULL,
date_cancel DATETIME DEFAULT NULL,
fk_user_cancel integer DEFAULT NULL,
detail_refuse varchar(250) DEFAULT NULL,
note_private text,
note_public text,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
import_key varchar(14),
extraparams varchar(255) -- EN: for other parameters with json format / FR: pour d'autres paramètres au format JSON
)
ENGINE=innodb;

-- EN: Add double approval columns if missing / FR: Ajoute les colonnes de double approbation si elles sont absentes
ALTER TABLE llx_holiday
ADD COLUMN date_approval2 DATETIME DEFAULT NULL AFTER fk_user_approve,
ADD COLUMN fk_user_approve2 integer DEFAULT NULL AFTER date_approval2;
1 change: 1 addition & 0 deletions htdocs/install/mysql/tables/llx_user.sql
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ create table llx_user
fk_user integer NULL, -- Supervisor, hierarchic parent
fk_user_expense_validator integer NULL,
fk_user_holiday_validator integer NULL,
fk_user_holiday_validator2 integer NULL, -- EN: forced second holiday approver / FR: second valideur des congés forcé

national_registration_number varchar(50),
idpers1 varchar(128),
Expand Down
2 changes: 2 additions & 0 deletions htdocs/langs/en_US/users.lang
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ CantDisableAnAdminUserWithMassActions=You can't disable users in a mass action w
CantEnableAnAdminUserWithMassActions=You can't enable users in a mass action when one of them is an admin user (like "%s"). Try to disable the admin user(s) manually, one by one from their dedicated page.
ForceUserExpenseValidator=Force expense report validator
ForceUserHolidayValidator=Force leave request validator
ForceUserHolidaySecondValidator=Force second-level leave approver
SecondValidatorIsSupervisorByDefault=Second approver defaults to the first approver's manager
ValidatorIsSupervisorByDefault=By default, the validator is the supervisor of the user. Keep empty to keep this behavior.
UserPersonalEmail=Personal email
UserPersonalMobile=Personal mobile phone
Expand Down
Loading
Loading