diff --git a/htdocs/core/lib/project.lib.php b/htdocs/core/lib/project.lib.php index 4776907192f0c..2999eb74ac2e3 100644 --- a/htdocs/core/lib/project.lib.php +++ b/htdocs/core/lib/project.lib.php @@ -5,7 +5,8 @@ * Copyright (C) 2018-2024 Frédéric France * Copyright (C) 2022 Charlene Benke * Copyright (C) 2023 Gauthier VERDOL - * Copyright (C) 2024 MDW + * Copyright (C) 2024 MDW + * Copyright (C) 2024 Vincent de Grandpré * * 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 @@ -691,6 +692,7 @@ function projectLinesa(&$inc, $parent, &$lines, &$level, $var, $showproject, &$t $taskstatic->planned_workload = $lines[$i]->planned_workload; $taskstatic->duration_effective = $lines[$i]->duration_effective; $taskstatic->budget_amount = $lines[$i]->budget_amount; + $taskstatic->billable = $lines[$i]->billable; // Action column if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) { @@ -932,6 +934,17 @@ function projectLinesa(&$inc, $parent, &$lines, &$level, $var, $showproject, &$t print ''; } + // Billable + if (count($arrayfields) > 0 && !empty($arrayfields['t.billable']['checked'])) { + print ''; + if ($lines[$i]->billable) { + print ''.$langs->trans('Yes').''; + } else { + print ''.$langs->trans('No').''; + } + print ''; + } + // Extra fields $extrafieldsobjectkey = $taskstatic->table_element; $extrafieldsobjectprefix = 'efpt.'; diff --git a/htdocs/core/modules/modProjet.class.php b/htdocs/core/modules/modProjet.class.php index d58edcf7de269..a34ea519d2ab0 100644 --- a/htdocs/core/modules/modProjet.class.php +++ b/htdocs/core/modules/modProjet.class.php @@ -266,7 +266,7 @@ public function __construct($db) $keyforaliasextra = 'extra'; include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php'; // Add fields for tasks - $this->export_fields_array[$r] = array_merge($this->export_fields_array[$r], array('pt.rowid'=>'TaskId', 'pt.ref'=>'RefTask', 'pt.label'=>'LabelTask', 'pt.dateo'=>"TaskDateStart", 'pt.datee'=>"TaskDateEnd", 'pt.duration_effective'=>"DurationEffective", 'pt.planned_workload'=>"PlannedWorkload", 'pt.progress'=>"Progress", 'pt.description'=>"TaskDescription")); + $this->export_fields_array[$r] = array_merge($this->export_fields_array[$r], array('pt.rowid'=>'TaskId', 'pt.ref'=>'RefTask', 'pt.label'=>'LabelTask', 'pt.dateo'=>"TaskDateStart", 'pt.datee'=>"TaskDateEnd", 'pt.duration_effective'=>"DurationEffective", 'pt.planned_workload'=>"PlannedWorkload", 'pt.progress'=>"Progress", 'pt.description'=>"TaskDescription", 'pt.billable'=>"Billable")); $this->export_entities_array[$r] = array_merge($this->export_entities_array[$r], array('pt.rowid'=>'projecttask', 'pt.ref'=>'projecttask', 'pt.label'=>'projecttask', 'pt.dateo'=>"projecttask", 'pt.datee'=>"projecttask", 'pt.duration_effective'=>"projecttask", 'pt.planned_workload'=>"projecttask", 'pt.progress'=>"projecttask", 'pt.description'=>"projecttask")); // Add extra fields for task $keyforselect = 'projet_task'; diff --git a/htdocs/langs/de_DE/projects.lang b/htdocs/langs/de_DE/projects.lang index a1cf6c11a82f1..a2fe1e07f22c1 100644 --- a/htdocs/langs/de_DE/projects.lang +++ b/htdocs/langs/de_DE/projects.lang @@ -302,3 +302,4 @@ NewLeadbyWeb=Ihre Nachricht bzw. Anfrage wurde erfasst. Wir werden uns so bald w NewLeadForm=Neues Kontaktformular LeadFromPublicForm=Online-Interessent per öffentlichem Formular ExportAccountingReportButtonLabel=Bericht erhalten +Billable = Verrechenbar diff --git a/htdocs/langs/en_US/projects.lang b/htdocs/langs/en_US/projects.lang index 5c0c2bde5fa01..c93a3744b4cf0 100644 --- a/htdocs/langs/en_US/projects.lang +++ b/htdocs/langs/en_US/projects.lang @@ -310,3 +310,4 @@ MergeTasks=Merge tasks TaskMergeSuccess=Tasks have been merged ErrorTaskIdIsMandatory=Error: Task id is mandatory ErrorsTaskMerge=An error occurred while merging tasks +Billable = Billable diff --git a/htdocs/langs/es_ES/projects.lang b/htdocs/langs/es_ES/projects.lang index 6a3714c1d9df4..89012d12a75c9 100644 --- a/htdocs/langs/es_ES/projects.lang +++ b/htdocs/langs/es_ES/projects.lang @@ -302,3 +302,4 @@ NewLeadbyWeb=Su mensaje o solicitud ha sido grabada. Le responderemos o contacta NewLeadForm=Nuevo formulario de contacto LeadFromPublicForm=Cliente potencial en línea desde un formulario público ExportAccountingReportButtonLabel=Obtener informe +Billable = Billable diff --git a/htdocs/langs/fr_FR/projects.lang b/htdocs/langs/fr_FR/projects.lang index 40df6cbc7b91e..295af5d64b743 100644 --- a/htdocs/langs/fr_FR/projects.lang +++ b/htdocs/langs/fr_FR/projects.lang @@ -310,3 +310,4 @@ MergeTasks=Fusionner tâches TaskMergeSuccess=Les tâches ont été fusionnées ErrorTaskIdIsMandatory=Erreur : l'ID de tâche est obligatoire ErrorsTaskMerge=Une erreur s'est produite lors de la fusion des tâches +Billable = Facturable diff --git a/htdocs/langs/it_IT/projects.lang b/htdocs/langs/it_IT/projects.lang index 7458811858d9a..80cb0a69404f9 100644 --- a/htdocs/langs/it_IT/projects.lang +++ b/htdocs/langs/it_IT/projects.lang @@ -302,3 +302,4 @@ NewLeadbyWeb=Il tuo messaggio o richiesta è stato registrato. Ti risponderemo o NewLeadForm=Nuovo modulo di contatto LeadFromPublicForm=Lead online da modulo pubblico ExportAccountingReportButtonLabel=Get report +Billable = Fatturabile diff --git a/htdocs/projet/class/task.class.php b/htdocs/projet/class/task.class.php index df77bf2268e2b..40bb5c1bee614 100644 --- a/htdocs/projet/class/task.class.php +++ b/htdocs/projet/class/task.class.php @@ -6,7 +6,8 @@ * Copyright (C) 2020 Juanjo Menent * Copyright (C) 2022 Charlene Benke * Copyright (C) 2023 Gauthier VERDOL - * Copyright (C) 2024 MDW + * Copyright (C) 2024 MDW + * Copyright (C) 2024 Vincent de Grandpré * * 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 @@ -270,6 +271,12 @@ class Task extends CommonObjectLine */ public $task_parent_position; + /** + * Status indicate whether the task is billable (time is meant to be added to invoice) '1' or not '0' + * @var int billable + */ + public $billable = 1; + /** * @var float budget_amount */ @@ -362,6 +369,7 @@ public function create($user, $notrigger = 0) $sql .= ", progress"; $sql .= ", budget_amount"; $sql .= ", priority"; + $sql .= ", billable"; $sql .= ") VALUES ("; $sql .= (!empty($this->entity) ? (int) $this->entity : (int) $conf->entity); $sql .= ", ".((int) $this->fk_project); @@ -379,6 +387,7 @@ public function create($user, $notrigger = 0) $sql .= ", ".(($this->progress != '' && $this->progress >= 0) ? ((int) $this->progress) : 'null'); $sql .= ", ".(($this->budget_amount != '' && $this->budget_amount >= 0) ? ((int) $this->budget_amount) : 'null'); $sql .= ", ".(($this->priority != '' && $this->priority >= 0) ? (int) $this->priority : 'null'); + $sql .= ", ".((int) $this->billable); $sql .= ")"; $this->db->begin(); @@ -456,7 +465,8 @@ public function fetch($id, $ref = '', $loadparentdata = 0) $sql .= " t.priority,"; $sql .= " t.note_private,"; $sql .= " t.note_public,"; - $sql .= " t.rang"; + $sql .= " t.rang,"; + $sql .= " t.billable"; if (!empty($loadparentdata)) { $sql .= ", t2.ref as task_parent_ref"; $sql .= ", t2.rang as task_parent_position"; @@ -508,6 +518,7 @@ public function fetch($id, $ref = '', $loadparentdata = 0) $this->task_parent_ref = $obj->task_parent_ref; $this->task_parent_position = $obj->task_parent_position; } + $this->billable = $obj->billable; // Retrieve all extrafield $this->fetch_optionals(); @@ -595,7 +606,8 @@ public function update($user = null, $notrigger = 0) $sql .= " progress=".(($this->progress != '' && $this->progress >= 0) ? $this->progress : 'null').","; $sql .= " budget_amount=".(($this->budget_amount != '' && $this->budget_amount >= 0) ? $this->budget_amount : 'null').","; $sql .= " rang=".((!empty($this->rang)) ? ((int) $this->rang) : "0").","; - $sql .= " priority=".((!empty($this->priority)) ? ((int) $this->priority) : "0"); + $sql .= " priority=".((!empty($this->priority)) ? ((int) $this->priority) : "0").","; + $sql .= " billable=".((int) $this->billable); $sql .= " WHERE rowid=".((int) $this->id); $this->db->begin(); @@ -1020,6 +1032,7 @@ public function initAsSpecimen() $this->priority = 0; $this->note_private = 'This is a specimen private note'; $this->note_public = 'This is a specimen public note'; + $this->billable = 1; return 1; } @@ -1063,7 +1076,7 @@ public function getTasksArray($usert = null, $userp = null, $projectid = 0, $soc $sql .= " p.rowid as projectid, p.ref, p.title as plabel, p.public, p.fk_statut as projectstatus, p.usage_bill_time,"; $sql .= " t.rowid as taskid, t.ref as taskref, t.label, t.description, t.fk_task_parent, t.duration_effective, t.progress, t.fk_statut as status,"; $sql .= " t.dateo as date_start, t.datee as date_end, t.planned_workload, t.rang, t.priority,"; - $sql .= " t.budget_amount,"; + $sql .= " t.budget_amount, t.billable,"; $sql .= " t.note_public, t.note_private,"; $sql .= " s.rowid as thirdparty_id, s.nom as thirdparty_name, s.email as thirdparty_email,"; $sql .= " p.fk_opp_status, p.opp_amount, p.opp_percent, p.budget_amount as project_budget_amount"; @@ -1178,7 +1191,7 @@ public function getTasksArray($usert = null, $userp = null, $projectid = 0, $soc $sql .= " t.datec, t.dateo, t.datee, t.tms,"; $sql .= " t.rowid, t.ref, t.label, t.description, t.fk_task_parent, t.duration_effective, t.progress, t.fk_statut,"; $sql .= " t.dateo, t.datee, t.planned_workload, t.rang, t.priority,"; - $sql .= " t.budget_amount,"; + $sql .= " t.budget_amount, t.billable,"; $sql .= " t.note_public, t.note_private,"; $sql .= " s.rowid, s.nom, s.email,"; $sql .= " p.fk_opp_status, p.opp_amount, p.opp_percent, p.budget_amount"; @@ -1273,6 +1286,8 @@ public function getTasksArray($usert = null, $userp = null, $projectid = 0, $soc $tasks[$i]->thirdparty_name = $obj->thirdparty_name; $tasks[$i]->thirdparty_email = $obj->thirdparty_email; + $tasks[$i]->billable = $obj->billable; + if ($loadextras) { if (!empty($extrafields->attributes['projet']['label'])) { foreach ($extrafields->attributes['projet']['label'] as $key => $val) { diff --git a/htdocs/projet/tasks.php b/htdocs/projet/tasks.php index ef97960d1262b..f1502d8ee9be0 100644 --- a/htdocs/projet/tasks.php +++ b/htdocs/projet/tasks.php @@ -89,6 +89,7 @@ $search_progresscalc = GETPOST('search_progresscalc'); $search_progressdeclare = GETPOST('search_progressdeclare'); $search_task_budget_amount = GETPOST('search_task_budget_amount'); +$search_task_billable = GETPOST('search_task_billable'); $search_date_start_startmonth = GETPOSTINT('search_date_start_startmonth'); $search_date_start_startyear = GETPOSTINT('search_date_start_startyear'); @@ -148,6 +149,7 @@ $progress = GETPOSTINT('progress'); $budget_amount = GETPOSTFLOAT('budget_amount'); +$billable = (GETPOST('billable', 'aZ') == 'yes'? 1 : 0); $label = GETPOST('label', 'alpha'); $description = GETPOST('description', 'restricthtml'); $planned_workloadhour = (GETPOSTISSET('planned_workloadhour') ? GETPOSTINT('planned_workloadhour') : ''); @@ -176,6 +178,7 @@ if ($object->usage_bill_time) { $arrayfields['t.tobill'] = array('label' => $langs->trans("TimeToBill"), 'checked' => 0, 'position' => 11); $arrayfields['t.billed'] = array('label' => $langs->trans("TimeBilled"), 'checked' => 0, 'position' => 12); + $arrayfields['t.billable'] = array('label' => $langs->trans("Billable"), 'checked' => 1, 'position' => 13); } // Extra fields @@ -234,6 +237,7 @@ $search_progresscalc = ''; $search_progressdeclare = ''; $search_task_budget_amount = ''; + $search_task_billable = ''; $toselect = array(); $search_array_options = array(); $search_date_start_startmonth = ""; @@ -315,6 +319,9 @@ if ($search_task_budget_amount) { $morewherefilterarray[] = natural_search('t.budget_amount', $search_task_budget_amount, 1, 1); } +if ($search_task_billable) { + $morewherefilterarray[] = " t.billable = ".($search_task_billable == "yes" ? 1 : 0); +} //var_dump($morewherefilterarray); $morewherefilter = ''; @@ -364,6 +371,7 @@ $task->date_end = $date_end; $task->progress = $progress; $task->budget_amount = $budget_amount; + $task->billable = $billable; // Fill array 'array_options' with data from add form $ret = $extrafields->setOptionalsFromPost(null, $task); @@ -557,6 +565,9 @@ if ($search_task_budget_amount) { $param .= '&search_task_budget_amount='.urlencode($search_task_budget_amount); } + if ($search_task_billable) { + $param .= '&search_task_billable='.urlencode($search_task_billable); + } if ($optioncss != '') { $param .= '&optioncss='.urlencode($optioncss); } @@ -796,6 +807,11 @@ } print ''; + // Billable + print ''.$langs->trans("Billable").''; + print $form->selectyesno('billable'); + print ''; + // Date start task print ''.$langs->trans("DateStart").''; print img_picto('', 'action', 'class="pictofixedwidth"'); @@ -1054,6 +1070,12 @@ print ''; } + if (!empty($arrayfields['t.billable']['checked'])) { + print ''; + print $form->selectyesno('search_task_billable', $search_task_billable, 0, false, 1); + print ''; + } + include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_input.tpl.php'; print ' '; @@ -1126,6 +1148,10 @@ if (!empty($arrayfields['c.assigned']['checked'])) { print_liste_field_titre($arrayfields['c.assigned']['label'], $_SERVER["PHP_SELF"], "", '', $param, '', $sortfield, $sortorder, 'center ', ''); } + + if (!empty($arrayfields['t.billable']['checked'])) { + print_liste_field_titre($arrayfields['t.billable']['label'], $_SERVER["PHP_SELF"], "", '', $param, '', $sortfield, $sortorder, 'center ', ''); + } // Extra fields $disablesortlink = 1; include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_title.tpl.php'; diff --git a/htdocs/projet/tasks/task.php b/htdocs/projet/tasks/task.php index fd8585c588dd2..42baa45edc03f 100644 --- a/htdocs/projet/tasks/task.php +++ b/htdocs/projet/tasks/task.php @@ -3,7 +3,8 @@ * Copyright (C) 2006-2017 Laurent Destailleur * Copyright (C) 2010-2012 Regis Houssin * Copyright (C) 2018 Frédéric France - * Copyright (C) 2024 MDW + * Copyright (C) 2024 MDW + * Copyright (C) 2024 Vincent de Grandpré * * 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 @@ -119,6 +120,7 @@ $object->date_end = dol_mktime(GETPOSTINT('date_endhour'), GETPOSTINT('date_endmin'), 0, GETPOSTINT('date_endmonth'), GETPOSTINT('date_endday'), GETPOSTINT('date_endyear')); $object->progress = price2num(GETPOST('progress', 'alphanohtml')); $object->budget_amount = GETPOSTFLOAT('budget_amount'); + $object->billable = (GETPOST('billable', 'aZ') == 'yes' ? 1 : 0); // Fill array 'array_options' with data from add form $ret = $extrafields->setOptionalsFromPost(null, $object, '@GETPOSTISSET'); @@ -509,6 +511,11 @@ print $formother->select_percent($object->progress, 'progress', 0, 5, 0, 100, 1); print ''; + // Billable + print ''.$langs->trans("Billable").''; + print $form->selectyesno('billable', $object->billable); + print ''; + // Description print ''.$langs->trans("Description").''; @@ -682,6 +689,11 @@ } print ''; + // Billable + print ''.$langs->trans("Billable").''; + print ''.($object->billable ? $langs->trans('Yes') : $langs->trans('No')).''; + print ''; + // Other attributes $cols = 3; $parameters = array('socid' => $socid); diff --git a/htdocs/projet/tasks/time.php b/htdocs/projet/tasks/time.php index 4d020e03ac08d..b65dce3385e5c 100644 --- a/htdocs/projet/tasks/time.php +++ b/htdocs/projet/tasks/time.php @@ -7,7 +7,8 @@ * Copyright (C) 2018 Frédéric France * Copyright (C) 2019-2021 Christophe Battarel * Copyright (C) 2023 Gauthier VERDOL - * Copyright (C) 2024 MDW + * Copyright (C) 2024 MDW + * Copyright (C) 2024 Vincent de Grandpré * * 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 @@ -1573,7 +1574,8 @@ function setDetailVisibility() { $sql .= " u.lastname, u.firstname, u.login, u.photo, u.gender, u.statut as user_status,"; $sql .= " il.fk_facture as invoice_id, inv.fk_statut,"; $sql .= " p.fk_soc,s.name_alias,"; - $sql .= " t.invoice_line_id"; + $sql .= " t.invoice_line_id,"; + $sql .= " pt.billable"; // Add fields from hooks $parameters = array(); $reshook = $hookmanager->executeHooks('printFieldListSelect', $parameters, $object); // Note that $action and $object may have been modified by hook @@ -2411,6 +2413,7 @@ function setDetailVisibility() { } // Invoiced + $invoiced = false; if (!empty($arrayfields['valuebilled']['checked'])) { print ''; // invoice_id and invoice_line_id if (!getDolGlobalString('PROJECT_HIDE_TASKS') && getDolGlobalString('PROJECT_BILL_TIME_SPENT')) { @@ -2432,8 +2435,13 @@ function setDetailVisibility() { } } } + $invoiced = true; } else { - print $langs->trans("No"); + if ( intval($task_time->billable) == 1) { + print $langs->trans("No"); + } else { + print $langs->trans("Disabled"); + } } } else { print '' . $langs->trans("NA") . ''; @@ -2485,7 +2493,17 @@ function setDetailVisibility() { $selected = 1; } print ' '; - print ''; + // Disable select if task not billable or already invoiced + $disabled = (intval($task_time->billable) !=1 || $invoiced); + $ctrl = ''; + if ($disabled) { + // If disabled, a dbl-click very close outside the control + // will re-enable it, so that user is not blocked if needed. + print ''.$ctrl.''; + print ''; + } else { + print $ctrl; + } } } } diff --git a/test/phpunit/ProjectTest.php b/test/phpunit/ProjectTest.php index 89ab46c146146..152bc0361db38 100644 --- a/test/phpunit/ProjectTest.php +++ b/test/phpunit/ProjectTest.php @@ -165,6 +165,7 @@ public function testTaskCreate($idproject) $localobject = new Task($db); $localobject->initAsSpecimen(); $localobject->fk_project = $idproject; + $localobject->billable = 1; $result = $localobject->create($user); $this->assertLessThan($result, 0);