diff --git a/ChangeLog b/ChangeLog
index bcded58d383ae..03c7fa2c4c0b1 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -452,6 +452,9 @@ NEW: Accountancy - Option to select the label of operation (#31200)
NEW: Accountancy - Allow grouping taxes with primary line price (#26732)
NEW: Activate PHPUnit with tests on permission on $action ==...
NEW: Add advice for max size on list for better performance
+NEW: Holiday module - Option "Les demandes de congés requièrent une double validation" in module settings
+NEW: Approbation de niveau 2 des demandes de congés
+NEW: Holiday module - Add "Responsable de seconde approbation" responsible field on holiday card
NEW: Add an advanced permission to validate knowledge (#30855)
NEW: Add a test mode into the setup of AI module to test the AI prompts.
NEW: Add a tool to decrypt data encrypted in database.
diff --git a/htdocs/admin/holiday.php b/htdocs/admin/holiday.php
index ca8d0732c7e64..ba6312c8f83b8 100644
--- a/htdocs/admin/holiday.php
+++ b/htdocs/admin/holiday.php
@@ -529,6 +529,8 @@
print "";
print "";
+// EN: Display toggle to consume holiday balance at end of month.
+// FR: Affiche le bascule pour consommer le solde de congés en fin de mois.
// Set holiday decrease at the end of month
print '
";
+// EN: Display toggle to require double approval for leave requests.
+// FR: Affiche le bascule pour exiger une double validation des demandes de congés.
+print '
';
@@ -1259,6 +1343,11 @@
$approverexpected = new User($db);
$approverexpected->fetch($object->fk_validator); // Use that should be the approver
+ $secondapproverexpected = new User($db);
+ if ($object->fk_user_approve2 > 0) {
+ // EN: Preload the second approver / FR: Précharger le second valideur
+ $secondapproverexpected->fetch($object->fk_user_approve2);
+ }
$userRequest = new User($db);
$userRequest->fetch($object->fk_user);
@@ -1491,11 +1580,29 @@
}
print '';
print '
';
+
+ // EN: Display the second approver / FR: Afficher le second valideur
+ if (getDolGlobalString('HOLIDAY_REQUIRE_DOUBLE_APPROVAL')) {
+ print '
';
$include_users = $object->fetch_users_approver_holiday();
+ // EN: Normalize the approvers list / FR: Normaliser la liste des valideurs
+ if (!is_array($include_users)) {
+ $include_users = array();
+ }
if (!in_array($object->fk_validator, $include_users)) { // Add the current validator to the list to not lose it when editing.
$include_users[] = $object->fk_validator;
}
@@ -1505,6 +1612,22 @@
$arrayofvalidatorstoexclude = (($user->admin || ($user->id != $userRequest->id)) ? '' : array($user->id)); // We exclude ourself from validator list. Not if we are admin or if we are on the leave of someone else
$s = $form->select_dolusers($object->fk_validator, "valideur", (($action == 'editvalidator') ? 0 : 1), $arrayofvalidatorstoexclude, 0, $include_users);
print $form->textwithpicto($s, $langs->trans("AnyOtherInThisListCanValidate"));
+ // EN: Offer edition of the second approver / FR: Permettre l'édition du second valideur
+ if (getDolGlobalString('HOLIDAY_REQUIRE_DOUBLE_APPROVAL')) {
+ if (!in_array($object->fk_user_approve2, $include_users)) {
+ $include_users[] = $object->fk_user_approve2;
+ }
+ // EN: Fetch the proposed second approver / FR: Récupérer le second valideur proposé
+ $defaultselectuser2 = $object->fk_user_approve2;
+ // EN: Preserve the posted value for the second approver / FR: Conserver la valeur transmise pour le second valideur
+ if ($action == 'editvalidator' && GETPOSTINT('valideur2') > 0) {
+ // EN: Refresh the second approver with the posted value / FR: Actualiser le second valideur avec la valeur transmise
+ $defaultselectuser2 = GETPOSTINT('valideur2');
+ }
+ $s2 = $form->select_dolusers($defaultselectuser2, "valideur2", (($action == 'editvalidator') ? 0 : 1), $arrayofvalidatorstoexclude, 0, $include_users);
+ print '
';
+ }
+
}
if ($action == 'editvalidator') {
print '';
diff --git a/htdocs/holiday/class/holiday.class.php b/htdocs/holiday/class/holiday.class.php
index a8ba624ba2dad..1dff6940cb684 100644
--- a/htdocs/holiday/class/holiday.class.php
+++ b/htdocs/holiday/class/holiday.class.php
@@ -131,6 +131,16 @@ class Holiday extends CommonObject
*/
public $fk_user_approve;
+ /**
+ * @var int Date of second approval / Date de la seconde approbation
+ */
+ public $date_approval2;
+
+ /**
+ * @var int ID for second approval / ID de la seconde approbation
+ */
+ public $fk_user_approve2;
+
/**
* @var int Date for refuse
*/
@@ -328,6 +338,7 @@ public function create($user, $notrigger = 0)
$sql .= "fk_validator,";
$sql .= "fk_type,";
$sql .= "fk_user_create,";
+ $sql .= "fk_user_approve2,";
$sql .= "entity";
$sql .= ") VALUES (";
$sql .= "'(PROV)',";
@@ -341,6 +352,8 @@ public function create($user, $notrigger = 0)
$sql .= " ".((int) $this->fk_validator).",";
$sql .= " ".((int) $this->fk_type).",";
$sql .= " ".((int) $user->id).",";
+ // EN: Persist the chosen second approver on creation / FR: Enregistrer le second valideur choisi à la création
+ $sql .= ($this->fk_user_approve2 > 0 ? " ".((int) $this->fk_user_approve2)."," : " null,");
$sql .= " ".((int) $conf->entity);
$sql .= ")";
@@ -425,6 +438,8 @@ public function fetch($id, $ref = '')
$sql .= " cp.fk_user_valid,";
$sql .= " cp.date_approval,";
$sql .= " cp.fk_user_approve,";
+ $sql .= " cp.date_approval2,";
+ $sql .= " cp.fk_user_approve2,";
$sql .= " cp.date_refuse,";
$sql .= " cp.fk_user_refuse,";
$sql .= " cp.date_cancel,";
@@ -466,6 +481,9 @@ public function fetch($id, $ref = '')
$this->user_validation_id = $obj->fk_user_valid;
$this->date_approval = $this->db->jdate($obj->date_approval);
$this->fk_user_approve = $obj->fk_user_approve;
+ // EN: Store the second approval metadata / FR: Enregistrer les détails de la seconde approbation
+ $this->date_approval2 = $this->db->jdate($obj->date_approval2);
+ $this->fk_user_approve2 = $obj->fk_user_approve2;
$this->date_refuse = $this->db->jdate($obj->date_refuse);
$this->fk_user_refuse = $obj->fk_user_refuse;
$this->date_cancel = $this->db->jdate($obj->date_cancel);
@@ -520,6 +538,8 @@ public function fetchByUser($user_id, $order = '', $filter = '')
$sql .= " cp.fk_user_valid,";
$sql .= " cp.date_approval,";
$sql .= " cp.fk_user_approve,";
+ $sql .= " cp.date_approval2,";
+ $sql .= " cp.fk_user_approve2,";
$sql .= " cp.date_refuse,";
$sql .= " cp.fk_user_refuse,";
$sql .= " cp.date_cancel,";
@@ -591,6 +611,9 @@ public function fetchByUser($user_id, $order = '', $filter = '')
$tab_result[$i]['fk_user_valid'] = $obj->fk_user_valid;
$tab_result[$i]['date_approval'] = $this->db->jdate($obj->date_approval);
$tab_result[$i]['fk_user_approve'] = $obj->fk_user_approve;
+ // EN: Keep the second approval data / FR: Conserver les détails de seconde approbation
+ $tab_result[$i]['date_approval2'] = $this->db->jdate($obj->date_approval2);
+ $tab_result[$i]['fk_user_approve2'] = $obj->fk_user_approve2;
$tab_result[$i]['date_refuse'] = $this->db->jdate($obj->date_refuse);
$tab_result[$i]['fk_user_refuse'] = $obj->fk_user_refuse;
$tab_result[$i]['date_cancel'] = $this->db->jdate($obj->date_cancel);
@@ -650,6 +673,8 @@ public function fetchAll($order, $filter)
$sql .= " cp.fk_user_valid,";
$sql .= " cp.date_approval,";
$sql .= " cp.fk_user_approve,";
+ $sql .= " cp.date_approval2,";
+ $sql .= " cp.fk_user_approve2,";
$sql .= " cp.date_refuse,";
$sql .= " cp.fk_user_refuse,";
$sql .= " cp.date_cancel,";
@@ -721,6 +746,9 @@ public function fetchAll($order, $filter)
$tab_result[$i]['fk_user_valid'] = $obj->fk_user_valid;
$tab_result[$i]['date_approval'] = $this->db->jdate($obj->date_approval);
$tab_result[$i]['fk_user_approve'] = $obj->fk_user_approve;
+ // EN: Keep the second approval data / FR: Conserver les détails de seconde approbation
+ $tab_result[$i]['date_approval2'] = $this->db->jdate($obj->date_approval2);
+ $tab_result[$i]['fk_user_approve2'] = $obj->fk_user_approve2;
$tab_result[$i]['date_refuse'] = $obj->date_refuse;
$tab_result[$i]['fk_user_refuse'] = $obj->fk_user_refuse;
$tab_result[$i]['date_cancel'] = $obj->date_cancel;
@@ -945,6 +973,17 @@ public function approve($user = null, $notrigger = 0)
} else {
$sql .= " fk_user_approve = NULL,";
}
+ // EN: Persist the second approval info / FR: Enregistrer les détails de seconde approbation
+ if (!empty($this->date_approval2)) {
+ $sql .= " date_approval2 = '".$this->db->idate($this->date_approval2)."',";
+ } else {
+ $sql .= " date_approval2 = NULL,";
+ }
+ if (!empty($this->fk_user_approve2)) {
+ $sql .= " fk_user_approve2 = ".((int) $this->fk_user_approve2).",";
+ } else {
+ $sql .= " fk_user_approve2 = NULL,";
+ }
if (!empty($this->date_refuse)) {
$sql .= " date_refuse = '".$this->db->idate($this->date_refuse)."',";
} else {
@@ -1075,6 +1114,17 @@ public function update($user = null, $notrigger = 0)
} else {
$sql .= " fk_user_approve = NULL,";
}
+ // EN: Persist the second approval info / FR: Enregistrer les détails de seconde approbation
+ if (!empty($this->date_approval2)) {
+ $sql .= " date_approval2 = '".$this->db->idate($this->date_approval2)."',";
+ } else {
+ $sql .= " date_approval2 = NULL,";
+ }
+ if (!empty($this->fk_user_approve2)) {
+ $sql .= " fk_user_approve2 = ".((int) $this->fk_user_approve2).",";
+ } else {
+ $sql .= " fk_user_approve2 = NULL,";
+ }
if (!empty($this->date_refuse)) {
$sql .= " date_refuse = '".$this->db->idate($this->date_refuse)."',";
} else {
diff --git a/htdocs/install/mysql/migration/20.0.0-21.0.0.sql b/htdocs/install/mysql/migration/20.0.0-21.0.0.sql
index c5dc2a64a90ab..9fc748bdf74f0 100644
--- a/htdocs/install/mysql/migration/20.0.0-21.0.0.sql
+++ b/htdocs/install/mysql/migration/20.0.0-21.0.0.sql
@@ -38,6 +38,9 @@ UPDATE llx_paiement SET ref = rowid WHERE ref IS NULL OR ref = '';
ALTER TABLE llx_c_holiday_types ADD COLUMN block_if_negative integer NOT NULL DEFAULT 0 AFTER fk_country;
ALTER TABLE llx_c_holiday_types ADD COLUMN sortorder smallint;
+-- EN: Support second approval columns for holidays / FR: Ajouter les colonnes de seconde approbation pour les congés
+ALTER TABLE llx_holiday ADD COLUMN date_approval2 datetime DEFAULT NULL;
+ALTER TABLE llx_holiday ADD COLUMN fk_user_approve2 integer DEFAULT NULL;
-- Clean very old temporary tables (created during v9 migration or repair)
diff --git a/htdocs/install/mysql/tables/llx_holiday.sql b/htdocs/install/mysql/tables/llx_holiday.sql
index 8a2af4515a8a8..cc464f01d7272 100644
--- a/htdocs/install/mysql/tables/llx_holiday.sql
+++ b/htdocs/install/mysql/tables/llx_holiday.sql
@@ -38,6 +38,8 @@ 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,
diff --git a/htdocs/langs/en_US/holiday.lang b/htdocs/langs/en_US/holiday.lang
index 829f76c123692..2b4f943ca416d 100644
--- a/htdocs/langs/en_US/holiday.lang
+++ b/htdocs/langs/en_US/holiday.lang
@@ -60,6 +60,9 @@ ErrorCantDeleteCP=Error you don't have the right to delete this leave request.
CantCreateCP=You don't have the right to make leave requests.
InvalidValidatorCP=You must choose the approver for your leave request.
InvalidValidator=The user chosen isn't an approver.
+SecondApproverRequired=You must choose the second approver for your leave request.
+InvalidSecondValidatorCP=The user chosen isn't authorized as a second approver.
+SecondApprovalResponsible=Second approval responsible
NoDateDebut=You must select a start date.
NoDateFin=You must select an end date.
ErrorDureeCP=Your leave request does not contain working day.
@@ -146,6 +149,7 @@ FreeLegalTextOnHolidays=Free text on PDF
WatermarkOnDraftHolidayCards=Watermarks on draft leave requests
HolidaysToApprove=Holidays to approve
NobodyHasPermissionToValidateHolidays=Nobody has permission to validate leave requests
+PermissionHolidayDoubleApproval=Confirm leave requests (Level 2 approval)
HolidayBalanceMonthlyUpdate=Monthly update of leave balance
XIsAUsualNonWorkingDay=Exclude %s from the leave calculation, as they are typically designated as non-working days.
BlockHolidayIfNegative=Block if balance negative
@@ -159,6 +163,7 @@ NumberDayAddMass=Number of day to add to the selection
ConfirmMassIncreaseHolidayQuestion=Are you sure you want to increase holiday of the %s selected record(s)?
HolidayQtyNotModified=Balance of remaining days for %s has not been changed
ConsumeHolidaysAtTheEndOfTheMonthTheyAreTakenAt=Consume holidays at the end of the month they are taken at
+HolidayRequireDoubleApproval=Leave requests require double validation
ConsumeHolidaysAtTheEndOfTheMonthTheyAreTakenAtBis=By default, they are consumed/deducted as soon as they are approved
PleaseDefineSomeTypeOfHolidayFirst=Please define first some types of holiday into the dictionary (Menu Home - Setup - dictionaries)
HolidayMailSenderAdress=Email address for holiday requests
diff --git a/htdocs/langs/fr_FR/holiday.lang b/htdocs/langs/fr_FR/holiday.lang
index 366db788d531c..885c9604fe5c9 100644
--- a/htdocs/langs/fr_FR/holiday.lang
+++ b/htdocs/langs/fr_FR/holiday.lang
@@ -59,6 +59,9 @@ ErrorCantDeleteCP=Erreur, vous n'avez pas le droit de supprimer cette demande de
CantCreateCP=Erreur, vous n'avez pas le droit de créer une demande de congés.
InvalidValidatorCP=Vous devez choisir un approbateur pour votre demande de congés.
InvalidValidator=L'utilisateur choisi n'est pas un approbateur.
+SecondApproverRequired=Vous devez choisir le second valideur pour votre demande de congé.
+InvalidSecondValidatorCP=L'utilisateur choisi n'est pas autorisé comme second valideur.
+SecondApprovalResponsible=Utilisateur responsable de la seconde approbation
NoDateDebut=Vous devez choisir une date de début.
NoDateFin=Vous devez choisir une date de fin.
ErrorDureeCP=Votre demande de congés payés ne contient aucun jour ouvré.
@@ -145,6 +148,7 @@ FreeLegalTextOnHolidays=Texte libre sur PDF
WatermarkOnDraftHolidayCards=Filigranes sur les demandes de congés brouillons
HolidaysToApprove=Vacances à approuver
NobodyHasPermissionToValidateHolidays=Aucun utilisateur ne dispose des permissions pour valider les demandes de congés
+PermissionHolidayDoubleApproval=Confirmer les demandes de congés (Approbation de niveau 2)
HolidayBalanceMonthlyUpdate=Mise à jour mensuelle du solde des congés
XIsAUsualNonWorkingDay=Exclure %s du calcul des congés, car ils sont généralement désignés comme des jours non ouvrables.
BlockHolidayIfNegative=Bloqué lorsque le solde est négatif
@@ -157,6 +161,8 @@ ConfirmMassIncreaseHoliday=Augmentation massive du solde des congés
NumberDayAddMass=Nombre de jours à ajouter à la sélection
ConfirmMassIncreaseHolidayQuestion=Êtes-vous sûr de vouloir augmenter les vacances du ou des %s enregistrement(s) sélectionnés ?
HolidayQtyNotModified=Le solde des jours restants pour %s n'a pas été modifié
+ConsumeHolidaysAtTheEndOfTheMonthTheyAreTakenAt=Consommer les jours de congés à la fin du mois où ils sont pris
+HolidayRequireDoubleApproval=Les demandes de congés requièrent une double validation
ConsumeHolidaysAtTheEndOfTheMonthTheyAreTakenAt=Décompte les congés à la fin du mois où ils sont pris
PleaseDefineSomeTypeOfHolidayFirst=Veuillez d'abord définir certains types de vacances dans le dictionnaire (Menu Accueil - Configuration - dictionnaires)
HolidayMailSenderAdress=Adresse e-mail pour les demandes de vacances