diff --git a/htdocs/conf/conf.php.example b/htdocs/conf/conf.php.example index 926bb2a137bb9..8253ddcc55a8c 100644 --- a/htdocs/conf/conf.php.example +++ b/htdocs/conf/conf.php.example @@ -417,9 +417,26 @@ $dolibarr_cron_allow_cli='0'; // 2 will allow custom PHP inside website features even if your PHP setup does not protect you against dangerous system calls (this may be dangerous if you don't have a RCE protection like SELnux or Apparmor). // Default value: '0' // Examples: '0', '1' or '2' - +// // $dolibarr_website_allow_custom_php='0'; +// dolibarr_allow_localurl_for_webhooks +// ==================================== +// Allow webhooks to use a local url +// Default value: '0' +// Examples: '1' +// +// $dolibarr_allow_localurl_for_webhooks = '0'; + +// dolibarr_allow_unsecured_select_in_extrafields_filter +// ===================================================== +// Allow the use of subrequests inside USF IN filters +// Default value: '0' +// Examples: '1' +// +// $dolibarr_allow_unsecured_select_in_extrafields_filter = '0'; + + // php_session_save_handler // ======================== diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 2e96ab1d3912e..db17e843d7148 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -6,6 +6,7 @@ * Copyright (C) 2024-2025 MDW * Copyright (C) 2024 Frédéric France * Copyright (C) 2024 Ferran Marcet + * Copyright (C) 2026 Pierre Ardoin * * 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 @@ -94,6 +95,127 @@ print $form->formconfirm($_SERVER['PHP_SELF'] . '?id=' . $object->id . $selected, $langs->trans('ConfirmMassClone'), '', 'clonetasks', $formquestion, '', 1, 300, 590); } +if (in_array($massaction, array('preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'), true) && is_object($objecttmp) && $objecttmp->element == 'project_task') { + dol_syslog(__FILE__." render pre-massaction modal for massaction='".$massaction."' selected=".count((array) $toselect), LOG_WARNING); + if (!$user->hasRight('projet', 'creer')) { + setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors'); + } else { + $tasksById = $objecttmp->getAuthorizedTasksForMassAction($user, $toselect); + dol_syslog(__FILE__." pre-massaction authorized tasks=".count($tasksById), LOG_WARNING); + if (empty($tasksById)) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } else { + if ($massaction == 'preupdate_selected_tasks_progress') { + $finalAction = 'update_selected_tasks_progress'; + } elseif ($massaction == 'preupdate_selected_tasks_start_date') { + $finalAction = 'update_selected_tasks_start_date'; + } else { + $finalAction = 'update_selected_tasks_deadline'; + } + $rowCount = count($tasksById); + $isDateAction = ($finalAction != 'update_selected_tasks_progress'); + $taskRowsHeight = $rowCount * 34; + $tableHeaderHeight = 34; + $updateTasksRowHeight = ($isDateAction ? 36 : 0); + $confirmQuestionRowHeight = 34; + $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 5; + $modalBodyHeight = min(700, max(220, $computedBodyHeight)); + $modalHeight = min(900, $modalBodyHeight + 220); + $formquestion = array(); + $currentProjectId = GETPOSTINT('id'); + if ($currentProjectId > 0) { + $formquestion[] = array('type' => 'hidden', 'name' => 'id', 'value' => $currentProjectId); + } + $formquestion[] = array('type' => 'hidden', 'name' => 'massactiontaskfinal', 'value' => $finalAction); + $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); + + if ($finalAction == 'update_selected_tasks_progress') { + $progressInputNames = array(); + $tablehtml = '
'; + $tablehtml .= ''; + foreach ($tasksById as $taskId => $taskDb) { + $inputname = 'task_progress_'.$taskId; + $progressInputNames[] = $inputname; + $tablehtml .= ''; + $tablehtml .= ''; + } + $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('Progress').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).' %
'; + $formquestion[] = array('type' => 'other', 'name' => implode(',', $progressInputNames), 'label' => '', 'value' => $tablehtml); + $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress'); + } else { + $dateInputNames = array(); + $keepDurationNames = array(); + $tablehtml = '
'; + $tablehtml .= '
'; + $tablehtml .= ''.$langs->trans('MassActionApplyDateToTasks').''; + $tablehtml .= ''; + $tablehtml .= ''; + $tablehtml .= '
'; + $tablehtml .= ''; + $tablehtml .= ''; + foreach ($tasksById as $taskId => $taskDb) { + $datetimeName = 'task_datetime_'.$taskId; + $keepdurationname = 'keep_duration_'.$taskId; + $dateInputNames[] = $datetimeName; + $keepDurationNames[] = $keepdurationname; + if ($finalAction == 'update_selected_tasks_start_date') { + $currenttimestamp = (!empty($taskDb->dateo) ? (int) $db->jdate($taskDb->dateo) : dol_now()); + } else { + $currenttimestamp = (!empty($taskDb->datee) ? (int) $db->jdate($taskDb->datee) : dol_now()); + } + $datetimevalue = dol_print_date($currenttimestamp, '%Y-%m-%dT%H:%M'); + $tablehtml .= ''; + $tablehtml .= ''; + $tablehtml .= ''; + } + $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('DateHour').''.$langs->trans('Duration').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).'
'; + $formquestion[] = array('type' => 'other', 'name' => implode(',', array_merge($dateInputNames, $keepDurationNames)), 'label' => '', 'value' => $tablehtml); + $titleform = ($finalAction == 'update_selected_tasks_start_date' ? $langs->trans('MassActionUpdateSelectedTasksStartDate') : $langs->trans('MassActionUpdateSelectedTasksDeadline')); + } + + $pageforconfirm = $_SERVER['PHP_SELF']; + if ($currentProjectId > 0) { + $pageforconfirm .= (strpos($pageforconfirm, '?') === false ? '?' : '&').'id='.$currentProjectId; + } + print $form->formconfirm($pageforconfirm, $titleform, $langs->trans('MassActionTaskPopupDescription'), $finalAction, $formquestion, '', 1, $modalHeight, 800, 0, 'Validate', 'Cancel'); + print ''; + } + } +} + if ($massaction == 'preaffecttag' && isModEnabled('category')) { require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php'; $categ = new Categorie($db); diff --git a/htdocs/langs/en_US/projects.lang b/htdocs/langs/en_US/projects.lang index 221a9b8ea6b2a..4994030bfc714 100644 --- a/htdocs/langs/en_US/projects.lang +++ b/htdocs/langs/en_US/projects.lang @@ -337,3 +337,16 @@ TaskHourlyRateUpdated=Task's hourly rate updated UpdateWithLastHourlyRate=Update all recorded time spent of each user with the last hourly rate of the user UpdateUndefinedWithLastHourlyRate=Update the time spent that has no hourly rate defined with the last hourly rate of the user EnterUsersHourlyRateFirst=First, enter the hourly rate for the user(s)... +MassActionCloseSelectedTasks=Close selected project tasks +MassActionUpdateSelectedTasksProgress=Change progress of selected project tasks +MassActionUpdateSelectedTasksStartDate=Change start date of selected project tasks +MassActionUpdateSelectedTasksDeadline=Change deadline of selected project tasks +MassActionKeepTaskDuration=Keep duration +MassActionTaskPopupDescription=Confirm the mass action on selected tasks. +MassActionSelectedTasksClosed=%s task(s) closed. +MassActionSelectedTasksProgressUpdated=%s task(s) progress updated. +MassActionSelectedTasksStartDateUpdated=%s task(s) start date updated. +MassActionSelectedTasksDeadlineUpdated=%s task(s) deadline updated. +MassActionInvalidTaskProgressValue=Invalid progress value for task ID %s. +MassActionInvalidTaskDateValue=Invalid date value for task ID %s. +MassActionApplyDateToTasks=Update tasks below diff --git a/htdocs/projet/class/task.class.php b/htdocs/projet/class/task.class.php index 79c4ba0c3c0de..544ff62c982f2 100644 --- a/htdocs/projet/class/task.class.php +++ b/htdocs/projet/class/task.class.php @@ -8,6 +8,7 @@ * Copyright (C) 2023 Gauthier VERDOL * Copyright (C) 2024-2025 MDW * Copyright (C) 2024 Vincent de Grandpré + * Copyright (C) 2026 Pierre Ardoin * * 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 @@ -620,6 +621,47 @@ public function fetch($id, $ref = '', $loadparentdata = 0) } } + /** + * Return authorized tasks for selected ids. + * + * @param User $user Current user + * @param array $toselect List of selected task ids + * @return array Tasks indexed by task id + */ + public function getAuthorizedTasksForMassAction($user, $toselect) + { + $toselect = array_unique(array_filter(array_map('intval', (array) $toselect))); + if (empty($toselect)) { + return array(); + } + + $projectstatic = new Project($this->db); + $projectlistfilter = ''; + if (!$user->hasRight('projet', 'all', 'lire')) { + $projectsListId = $projectstatic->getProjectsAuthorizedForUser($user, 0, 1, 0); + $projectlistfilter = " AND p.rowid IN (".$this->db->sanitize($projectsListId ? $projectsListId : '0').")"; + } + + $sql = "SELECT t.rowid, t.ref, t.label, t.dateo, t.datee, t.progress, t.fk_statut, t.fk_projet"; + $sql .= " FROM ".MAIN_DB_PREFIX."projet_task AS t"; + $sql .= " INNER JOIN ".MAIN_DB_PREFIX."projet AS p ON p.rowid = t.fk_projet"; + $sql .= " WHERE t.rowid IN (".implode(',', $toselect).")"; + $sql .= " AND p.entity IN (".getEntity('project').")"; + $sql .= $projectlistfilter; + + $resql = $this->db->query($sql); + if (!$resql) { + return array(); + } + + $tasksById = array(); + while ($obj = $this->db->fetch_object($resql)) { + $tasksById[(int) $obj->rowid] = $obj; + } + + return $tasksById; + } + /** * Update database diff --git a/htdocs/projet/tasks.php b/htdocs/projet/tasks.php index 546331f1580c9..fee901afedd25 100644 --- a/htdocs/projet/tasks.php +++ b/htdocs/projet/tasks.php @@ -4,6 +4,7 @@ * Copyright (C) 2005-2017 Regis Houssin * Copyright (C) 2024-2025 MDW * Copyright (C) 2024-2025 Frédéric France + * Copyright (C) 2026 Pierre Ardoin * * 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 @@ -216,7 +217,7 @@ $action = 'list'; $massaction = ''; } -if (!GETPOST('confirmmassaction', 'alpha') && $massaction != 'presend' && $massaction != 'confirm_presend') { +if (!GETPOST('confirmmassaction', 'alpha') && !in_array($massaction, array('presend', 'confirm_presend', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline', 'close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true)) { $massaction = ''; } @@ -272,9 +273,123 @@ $objectclass = 'Task'; $objectlabel = 'Tasks'; $permissiontoread = $user->hasRight('projet', 'lire'); + $permissiontocreate = $user->hasRight('projet', 'creer'); $permissiontodelete = $user->hasRight('projet', 'supprimer'); $uploaddir = $conf->project->dir_output.'/tasks'; include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php'; + + $effectiveMassAction = ''; + $massactiontaskfinal = GETPOST('massactiontaskfinal', 'aZ09'); + dol_syslog(__FILE__." massaction='".$massaction."' action='".$action."' confirm='".$confirm."' massactiontaskfinal='".$massactiontaskfinal."'", LOG_WARNING); + if (in_array($action, array('close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $action; + } elseif (in_array($massactiontaskfinal, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massactiontaskfinal; + } elseif (in_array($massaction, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massaction; + } elseif ($massaction == 'close_selected_tasks' && GETPOST('confirmmassaction', 'alpha')) { + $effectiveMassAction = $massaction; + } + if ($permissiontocreate && !empty($effectiveMassAction)) { + $toselectpost = GETPOST('toselect', 'array:int'); + if (empty($toselectpost)) { + $toselectcsv = GETPOST('toselect', 'alphanohtml'); + if (!empty($toselectcsv)) { + $toselectpost = array_map('intval', explode(',', $toselectcsv)); + } + } + $tasksById = $taskstatic->getAuthorizedTasksForMassAction($user, $toselectpost); + dol_syslog(__FILE__." effectiveMassAction='".$effectiveMassAction."' selected=".count($toselectpost)." authorized=".count($tasksById), LOG_WARNING); + if (empty($tasksById)) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } else { + $error = 0; + $done = 0; + $db->begin(); + foreach ($tasksById as $taskId => $taskDb) { + $task = new Task($db); + if ($task->fetch($taskId) <= 0) { + $error++; + continue; + } + + if ($effectiveMassAction == 'close_selected_tasks') { + $task->progress = 100; + $task->status = Task::STATUS_CLOSED; + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + $progressraw = GETPOST('task_progress_'.$taskId, 'alphanohtml'); + if ($progressraw === '' || !is_numeric($progressraw)) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskProgressValue', $taskId); + continue; + } + $task->progress = max(0, min(100, (int) $progressraw)); + $task->status = ($task->progress >= 100 ? Task::STATUS_CLOSED : Task::STATUS_ONGOING); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date' || $effectiveMassAction == 'update_selected_tasks_deadline') { + $taskdatetime = GETPOST('task_datetime_'.$taskId, 'alphanohtml'); + $tasktimestamp = 0; + if (!empty($taskdatetime)) { + $tasktimestamp = dol_stringtotime(str_replace('T', ' ', $taskdatetime), 1); + } + if (empty($tasktimestamp) || $tasktimestamp < 0) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskDateValue', $taskId); + continue; + } + $keepduration = GETPOSTINT('keep_duration_'.$taskId); + $oldstart = (!empty($task->date_start) ? (int) $task->date_start : 0); + $oldend = (!empty($task->date_end) ? (int) $task->date_end : 0); + $durationseconds = ($oldstart > 0 && $oldend > 0 ? ($oldend - $oldstart) : null); + + if ($effectiveMassAction == 'update_selected_tasks_start_date') { + $task->date_start = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_end = $tasktimestamp + $durationseconds; + } + } else { + $task->date_end = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_start = $tasktimestamp - $durationseconds; + } + } + } + + if ($task->update($user) <= 0) { + $error++; + if (!empty($task->errors)) { + setEventMessages('', $task->errors, 'errors'); + } else { + setEventMessages($task->error, null, 'errors'); + } + } else { + $done++; + } + } + + if ($error) { + $db->rollback(); + } else { + $db->commit(); + } + + if ($done > 0 && !$error) { + if ($effectiveMassAction == 'close_selected_tasks') { + setEventMessages($langs->trans('MassActionSelectedTasksClosed', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + setEventMessages($langs->trans('MassActionSelectedTasksProgressUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date') { + setEventMessages($langs->trans('MassActionSelectedTasksStartDateUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_deadline') { + setEventMessages($langs->trans('MassActionSelectedTasksDeadlineUpdated', $done), null, 'mesgs'); + } + } elseif (!$error) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } + } + + $action = 'list'; + $massaction = ''; + } } $morewherefilterarray = array(); @@ -607,11 +722,15 @@ $arrayofmassactions = array(); if ($user->hasRight('projet', 'creer')) { $arrayofmassactions['preclonetasks'] = img_picto('', 'clone', 'class="pictofixedwidth"').$langs->trans("Clone"); + $arrayofmassactions['close_selected_tasks'] = img_picto('', 'tick', 'class="pictofixedwidth"').$langs->trans("MassActionCloseSelectedTasks"); + $arrayofmassactions['preupdate_selected_tasks_progress'] = img_picto('', 'projecttask', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksProgress"); + $arrayofmassactions['preupdate_selected_tasks_start_date'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksStartDate"); + $arrayofmassactions['preupdate_selected_tasks_deadline'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksDeadline"); } if ($permissiontodelete) { $arrayofmassactions['predelete'] = img_picto('', 'delete', 'class="pictofixedwidth"').$langs->trans("Delete"); } - if (in_array($massaction, array('presend', 'predelete'))) { + if (in_array($massaction, array('presend', 'predelete', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'), true)) { $arrayofmassactions = array(); } $massactionbutton = $form->selectMassAction('', $arrayofmassactions); diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index 7d3b21a12a433..3d0325d4dc94b 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -7,6 +7,7 @@ * Copyright (C) 2023 Gauthier VERDOL * Copyright (C) 2024-2025 MDW * Copyright (C) 2024-2025 Frédéric France + * Copyright (C) 2026 Pierre Ardoin * * 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 @@ -212,7 +213,7 @@ $action = 'list'; $massaction = ''; } -if (!GETPOST('confirmmassaction', 'alpha') && $massaction != 'presend' && $massaction != 'confirm_presend') { +if (!GETPOST('confirmmassaction', 'alpha') && !in_array($massaction, array('presend', 'confirm_presend', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline', 'close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true)) { $massaction = ''; } @@ -270,6 +271,133 @@ $objectlabel = 'Tasks'; $uploaddir = $conf->project->dir_output.'/tasks'; include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php'; + + $effectiveMassAction = ''; + $massactiontaskfinal = GETPOST('massactiontaskfinal', 'aZ09'); + dol_syslog(__FILE__." massaction='".$massaction."' action='".$action."' confirm='".$confirm."' massactiontaskfinal='".$massactiontaskfinal."'", LOG_WARNING); + if (in_array($action, array('close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $action; + } elseif (in_array($massactiontaskfinal, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massactiontaskfinal; + } elseif (in_array($massaction, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massaction; + } elseif ($massaction == 'close_selected_tasks' && GETPOST('confirmmassaction', 'alpha')) { + $effectiveMassAction = $massaction; + } + if ($permissiontocreate && !empty($effectiveMassAction)) { + $toselectpost = GETPOST('toselect', 'array:int'); + if (empty($toselectpost)) { + $toselectcsv = GETPOST('toselect', 'alphanohtml'); + if (!empty($toselectcsv)) { + $toselectpost = array_map('intval', explode(',', $toselectcsv)); + } + } + $tasksById = $object->getAuthorizedTasksForMassAction($user, $toselectpost); + dol_syslog(__FILE__." effectiveMassAction='".$effectiveMassAction."' selected=".count($toselectpost)." authorized=".count($tasksById), LOG_WARNING); + if (empty($tasksById)) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } else { + $error = 0; + $done = 0; + dol_syslog(__FILE__." start update loop action='".$effectiveMassAction."' ids=".implode(',', array_keys($tasksById)), LOG_WARNING); + $db->begin(); + foreach ($tasksById as $taskId => $taskDb) { + $task = new Task($db); + if ($task->fetch($taskId) <= 0) { + $error++; + dol_syslog(__FILE__." fetch failed for taskId=".$taskId, LOG_WARNING); + $task->error = empty($task->error) ? $langs->trans('ErrorRecordNotFound') : $task->error; + $task->errors[] = $task->error; + continue; + } + + if ($effectiveMassAction == 'close_selected_tasks') { + $task->progress = 100; + $task->status = Task::STATUS_CLOSED; + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + $progressraw = GETPOST('task_progress_'.$taskId, 'alphanohtml'); + dol_syslog(__FILE__." progress payload taskId=".$taskId." value='".$progressraw."'", LOG_WARNING); + if ($progressraw === '' || !is_numeric($progressraw)) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskProgressValue', $taskId); + continue; + } + $task->progress = max(0, min(100, (int) $progressraw)); + $task->status = ($task->progress >= 100 ? Task::STATUS_CLOSED : Task::STATUS_ONGOING); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date' || $effectiveMassAction == 'update_selected_tasks_deadline') { + $taskdatetime = GETPOST('task_datetime_'.$taskId, 'alphanohtml'); + dol_syslog(__FILE__." datetime payload taskId=".$taskId." value='".$taskdatetime."' keep_duration=".GETPOSTINT('keep_duration_'.$taskId), LOG_WARNING); + $tasktimestamp = 0; + if (!empty($taskdatetime)) { + $tasktimestamp = dol_stringtotime(str_replace('T', ' ', $taskdatetime), 1); + } + if (empty($tasktimestamp) || $tasktimestamp < 0) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskDateValue', $taskId); + continue; + } + $keepduration = GETPOSTINT('keep_duration_'.$taskId); + $oldstart = (!empty($task->date_start) ? (int) $task->date_start : 0); + $oldend = (!empty($task->date_end) ? (int) $task->date_end : 0); + $durationseconds = ($oldstart > 0 && $oldend > 0 ? ($oldend - $oldstart) : null); + + if ($effectiveMassAction == 'update_selected_tasks_start_date') { + $task->date_start = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_end = $tasktimestamp + $durationseconds; + } + } else { + $task->date_end = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_start = $tasktimestamp - $durationseconds; + } + } + } + + if ($task->update($user) <= 0) { + $error++; + dol_syslog(__FILE__." update failed for taskId=".$taskId." error=".$task->error, LOG_WARNING); + if (!empty($task->errors)) { + setEventMessages('', $task->errors, 'errors'); + } else { + setEventMessages($task->error, null, 'errors'); + } + } else { + $done++; + dol_syslog(__FILE__." update success for taskId=".$taskId." action=".$effectiveMassAction, LOG_WARNING); + } + } + + if ($error) { + $db->rollback(); + dol_syslog(__FILE__." rollback action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); + } else { + $db->commit(); + dol_syslog(__FILE__." commit action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); + } + + if ($done > 0 && !$error) { + if ($effectiveMassAction == 'close_selected_tasks') { + setEventMessages($langs->trans('MassActionSelectedTasksClosed', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + setEventMessages($langs->trans('MassActionSelectedTasksProgressUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date') { + setEventMessages($langs->trans('MassActionSelectedTasksStartDateUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_deadline') { + setEventMessages($langs->trans('MassActionSelectedTasksDeadlineUpdated', $done), null, 'mesgs'); + } + dol_syslog(__FILE__." success message sent action='".$effectiveMassAction."' done=".$done, LOG_WARNING); + } elseif (!$error) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + dol_syslog(__FILE__." warning message sent action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); + } + } + + $action = 'list'; + $massaction = ''; + } else { + dol_syslog(__FILE__." no effective mass action resolved (massaction='".$massaction."', action='".$action."', confirm='".$confirm."', massactiontaskfinal='".$massactiontaskfinal."')", LOG_WARNING); + } } // already done at line 85 @@ -734,7 +862,13 @@ if (!empty($permissiontodelete)) { $arrayofmassactions['predelete'] = img_picto('', 'delete', 'class="pictofixedwidth"').$langs->trans("Delete"); } -if (GETPOSTINT('nomassaction') || in_array($massaction, array('presend', 'predelete'))) { +if ($permissiontocreate) { + $arrayofmassactions['close_selected_tasks'] = img_picto('', 'tick', 'class="pictofixedwidth"').$langs->trans("MassActionCloseSelectedTasks"); + $arrayofmassactions['preupdate_selected_tasks_progress'] = img_picto('', 'projecttask', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksProgress"); + $arrayofmassactions['preupdate_selected_tasks_start_date'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksStartDate"); + $arrayofmassactions['preupdate_selected_tasks_deadline'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksDeadline"); +} +if (GETPOSTINT('nomassaction') || in_array($massaction, array('presend', 'predelete', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'))) { $arrayofmassactions = array(); } $massactionbutton = $form->selectMassAction('', $arrayofmassactions);