diff --git a/ChangeLog.md b/ChangeLog.md index 36eb6bf..cfaa082 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,9 @@ # CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) -## 1.6.0 (20/06/2026) +## 1.6.1 (08/12/2025) +- Fige les heures contractuelles dans la fiche hebdomadaire et les PDF pour conserver le contexte en cas d'évolution du contrat. / Freezes contract hours in the weekly card and PDFs to preserve context when an employee contract changes. + +## 1.6.0 (05/12/2025) - Ajoute un rappel hebdomadaire automatique par email configurable (activation, jour, heure, modèle) et une tâche planifiée dédiée avec bouton de test administrateur. / Adds a configurable automatic weekly email reminder (enablement, day, time, template) plus a dedicated scheduled task and admin test button. diff --git a/README.md b/README.md index 364e614..9e0ea0e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ TimesheetWeek ajoute une gestion hebdomadaire des feuilles de temps fidèle à l - Saisie dédiée pour les salariés en forfait jour grâce à des sélecteurs Journée/Matin/Après-midi convertissant automatiquement les heures. - Rappel hebdomadaire automatique par email configurable (activation, jour, heure, modèle) avec tâche planifiée dédiée et bouton d'envoi de test administrateur. - Affichage des compteurs dans la liste hebdomadaire et ajout du libellé « Zone » sur chaque sélecteur quotidien pour clarifier la saisie. +- Capture les heures au contrat au moment de la soumission pour figer le calcul des heures supplémentaires et les PDF, même si le contrat salarié évolue ensuite. - Ligne de total en bas de la liste hebdomadaire pour additionner heures, zones, paniers et afficher la colonne de date de validation. - Création rapide d'une feuille d'heures via le raccourci « Ajouter » du menu supérieur. - Compatibilité Multicompany pour partager les feuilles et leur numérotation, avec options de partage dédiées et filtres multi-sélection harmonisés à l'interface native. @@ -54,6 +55,7 @@ TimesheetWeek delivers weekly timesheet management that follows Dolibarr design - Dedicated input for daily rate employees with Full day/Morning/Afternoon selectors that automatically convert hours. - Configurable automatic weekly email reminder (enablement, weekday, time, template) with a dedicated scheduled task and admin test send button. - Counter display inside the weekly list plus a « Zone » caption on each daily selector for better input guidance. +- Snapshots contract hours at submission so overtime calculations and PDFs stay aligned even if the employee contract changes later. - Total row at the bottom of the weekly list to sum hours, zones, meals and expose the validation date column. - Quick creation shortcut available from the top-right « Add » menu. - Multicompany compatibility for sharing timesheets and numbering sequences, with dedicated sharing options and native-aligned multi-select filters. diff --git a/class/timesheetweek.class.php b/class/timesheetweek.class.php index e4035d7..32e7993 100644 --- a/class/timesheetweek.class.php +++ b/class/timesheetweek.class.php @@ -50,6 +50,7 @@ class TimesheetWeek extends CommonObject public $total_hours = 0.0; // total week hours public $overtime_hours = 0.0; // overtime based on user weeklyhours + public $contract = null; // stored contract hours snapshot public $zone1_count = 0; // zone 1 days count / nombre de jours en zone 1 public $zone2_count = 0; // zone 2 days count / nombre de jours en zone 2 public $zone3_count = 0; // zone 3 days count / nombre de jours en zone 3 @@ -141,6 +142,7 @@ public function create($user) 'fk_user_valid', 'total_hours', 'overtime_hours', + 'contract', 'zone1_count', 'zone2_count', 'zone3_count', @@ -160,6 +162,7 @@ public function create($user) (!empty($this->fk_user_valid) ? (int) $this->fk_user_valid : 'NULL'), (float) ($this->total_hours ?: 0), (float) ($this->overtime_hours ?: 0), + ($this->contract !== null ? (float) $this->contract : 'NULL'), (int) ($this->zone1_count ?: 0), (int) ($this->zone2_count ?: 0), (int) ($this->zone3_count ?: 0), @@ -279,7 +282,7 @@ public function fetch($id = null, $ref = null) $includeModelPdf = $this->checkModelPdfColumnAvailability(); $sql = "SELECT t.rowid, t.ref, t.entity, t.fk_user, t.year, t.week, t.status, t.note, t.date_creation, t.tms, t.date_validation, t.fk_user_valid,"; - $sql .= " t.total_hours, t.overtime_hours, t.zone1_count, t.zone2_count, t.zone3_count, t.zone4_count, t.zone5_count, t.meal_count"; +$sql .= " t.total_hours, t.overtime_hours, t.contract, t.zone1_count, t.zone2_count, t.zone3_count, t.zone4_count, t.zone5_count, t.meal_count"; if ($includeModelPdf) { $sql .= ", t.model_pdf"; } @@ -317,10 +320,11 @@ public function fetch($id = null, $ref = null) $this->date_creation = $this->db->jdate($obj->date_creation); $this->tms = $this->db->jdate($obj->tms); $this->date_validation = $this->db->jdate($obj->date_validation); - $this->fk_user_valid = (int) $obj->fk_user_valid; - $this->total_hours = (float) $obj->total_hours; - $this->overtime_hours = (float) $obj->overtime_hours; - $this->zone1_count = (int) $obj->zone1_count; +$this->fk_user_valid = (int) $obj->fk_user_valid; +$this->total_hours = (float) $obj->total_hours; +$this->overtime_hours = (float) $obj->overtime_hours; +$this->contract = ($obj->contract !== null ? (float) $obj->contract : null); +$this->zone1_count = (int) $obj->zone1_count; $this->zone2_count = (int) $obj->zone2_count; $this->zone3_count = (int) $obj->zone3_count; $this->zone4_count = (int) $obj->zone4_count; @@ -404,11 +408,12 @@ public function update($user) if ($this->year) $sets[] = "year=".(int) $this->year; if ($this->week) $sets[] = "week=".(int) $this->week; if ($this->status !== null) $sets[] = "status=".(int) $this->status; - $sets[] = "note=".($this->note !== null ? "'".$this->db->escape($this->note)."'" : "NULL"); - $sets[] = "fk_user_valid=".(!empty($this->fk_user_valid) ? (int) $this->fk_user_valid : "NULL"); - $sets[] = "total_hours=".(float) ($this->total_hours ?: 0); - $sets[] = "overtime_hours=".(float) ($this->overtime_hours ?: 0); - $sets[] = "zone1_count=".(int) ($this->zone1_count ?: 0); +$sets[] = "note=".($this->note !== null ? "'".$this->db->escape($this->note)."'" : "NULL"); +$sets[] = "fk_user_valid=".(!empty($this->fk_user_valid) ? (int) $this->fk_user_valid : "NULL"); +$sets[] = "total_hours=".(float) ($this->total_hours ?: 0); +$sets[] = "overtime_hours=".(float) ($this->overtime_hours ?: 0); +$sets[] = "contract=".($this->contract !== null ? (float) $this->contract : 'NULL'); +$sets[] = "zone1_count=".(int) ($this->zone1_count ?: 0); $sets[] = "zone2_count=".(int) ($this->zone2_count ?: 0); $sets[] = "zone3_count=".(int) ($this->zone3_count ?: 0); $sets[] = "zone4_count=".(int) ($this->zone4_count ?: 0); @@ -669,27 +674,38 @@ public function computeTotals() $this->zone5_count = $zoneBuckets[5]; $this->meal_count = $mealDays; - // Weekly contracted hours from user - $weekly = 35.0; + // Weekly contracted hours snapshot + $weekly = ($this->contract !== null ? (float) $this->contract : 35.0); if (!empty($this->fk_user)) { $u = new User($this->db); if ($u->fetch($this->fk_user) > 0) { - if (!empty($u->weeklyhours)) $weekly = (float) $u->weeklyhours; + if (!empty($u->weeklyhours)) { + $weekly = ($this->contract !== null ? (float) $this->contract : (float) $u->weeklyhours); + if ($this->contract === null) { + $this->contract = (float) $u->weeklyhours; + } + } } } + if ($this->contract === null) { + $this->contract = $weekly; + } $ot = $total - $weekly; $this->overtime_hours = ($ot > 0) ? $ot : 0.0; } /** - * Save totals into DB - * @return int - */ + * EN: Save totals into DB. + * FR: Enregistre les totaux en base. + * + * @return int + */ public function updateTotalsInDB() { $this->computeTotals(); $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element; $sql .= " SET total_hours=".(float) $this->total_hours.", overtime_hours=".(float) $this->overtime_hours; + $sql .= ", contract=".($this->contract !== null ? (float) $this->contract : 'NULL'); $sql .= ", zone1_count=".(int) $this->zone1_count.", zone2_count=".(int) $this->zone2_count; $sql .= ", zone3_count=".(int) $this->zone3_count.", zone4_count=".(int) $this->zone4_count; $sql .= ", zone5_count=".(int) $this->zone5_count.", meal_count=".(int) $this->meal_count; @@ -738,6 +754,19 @@ public function submit($user) $this->db->begin(); + $contractHours = 35.0; + $employeeContract = new User($this->db); + if ($employeeContract->fetch($this->fk_user) > 0 && !empty($employeeContract->weeklyhours)) { + $contractHours = (float) $employeeContract->weeklyhours; + } + $this->contract = $contractHours; + + if ($this->updateTotalsInDB() < 0) { + $this->db->rollback(); + $this->error = $this->db->lasterror(); + return -1; + } + // Set definitive ref if provisional if (empty($this->ref) || strpos($this->ref, '(PROV') === 0) { $newref = $this->generateDefinitiveRef(); @@ -780,81 +809,79 @@ public function submit($user) $this->db->commit(); return 1; } + /** + * Revert to draft + * @param User $user + * @return int + */ + public function revertToDraft($user) + { + $now = dol_now(); + $previousStatus = (int) $this->status; + + if ((int) $this->status === self::STATUS_DRAFT) { + $this->error = 'AlreadyDraft'; + return 0; + } + + $taskIds = array(); + $lines = $this->getLines(); + if (!empty($lines)) { + foreach ($lines as $line) { + if (empty($line->fk_task)) { + continue; + } - /** - * Revert to draft - * @param User $user - * @return int - */ - public function revertToDraft($user) - { - $now = dol_now(); - $previousStatus = (int) $this->status; - - if ((int) $this->status === self::STATUS_DRAFT) { - $this->error = 'AlreadyDraft'; - return 0; - } - - $taskIds = array(); - $lines = $this->getLines(); - if (!empty($lines)) { - foreach ($lines as $line) { - if (empty($line->fk_task)) { - continue; - } - - // EN: Track tasks with recorded hours to refresh effective durations after rollback. - // FR: Suit les tâches ayant des heures saisies pour rafraîchir les durées effectives après retour en brouillon. - if (((float) $line->hours) > 0) { - $taskIds[] = (int) $line->fk_task; - } - } - } - - $this->db->begin(); - - $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element. - " SET status=".(int) self::STATUS_DRAFT.", tms='".$this->db->idate($now)."', date_validation=NULL WHERE rowid=".(int) $this->id; - // EN: Enforce the entity scope while reverting to draft. - // FR: Impose la portée d'entité lors du retour en brouillon. - $sql .= " AND entity IN (".getEntity('timesheetweek').")"; - if (!$this->db->query($sql)) { - $this->db->rollback(); - $this->error = $this->db->lasterror(); - return -1; - } - - // Remove synced time entries to keep the ERP aligned (EN) - // Supprime les temps synchronisés pour garder l'ERP aligné (FR) - if ($this->deleteElementTimeRecords() < 0) { - $this->db->rollback(); - return -1; - } - - if (!empty($taskIds) && ($previousStatus === self::STATUS_APPROVED || $previousStatus === self::STATUS_SEALED)) { - // EN: Recompute effective duration on related tasks after removing approved times. - // FR: Recalcule la durée effective des tâches concernées après suppression des temps approuvés. - if ($this->updateTasksDurationEffective($taskIds) < 0) { - $this->db->rollback(); - return -1; - } - } - - $this->status = self::STATUS_DRAFT; - $this->tms = $now; - $this->date_validation = null; - - if (!$this->createAgendaEvent($user, 'TSWK_REOPEN', 'TimesheetWeekAgendaReopened', array($this->ref))) { - $this->db->rollback(); - return -1; - } - - $this->db->commit(); - - return 1; - } + // EN: Track tasks with recorded hours to refresh effective durations after rollback. + // FR: Suit les tâches ayant du temps saisi pour rafraîchir les durées effectives après retour. + $taskIds[(int) $line->fk_task] = (int) $line->fk_task; + } + } + + $this->db->begin(); + $up = "UPDATE ".MAIN_DB_PREFIX.$this->table_element; + $up .= " SET status=".(int) self::STATUS_DRAFT.", tms='".$this->db->idate($now)."', date_validation=NULL"; + // EN: Protect draft rollback with entity scoping for multi-company safety. + // FR: Protège le retour en brouillon en restreignant l'entité pour la sécurité multi-entreprise. + $up .= " WHERE rowid=".(int) $this->id; + $up .= " AND entity IN (".getEntity('timesheetweek').")"; + if (!$this->db->query($up)) { + $this->db->rollback(); + $this->error = $this->db->lasterror(); + return -1; + } + + if ($previousStatus === self::STATUS_APPROVED || $previousStatus === self::STATUS_SEALED) { + $this->overtime_hours = 0; + $this->updateTotalsInDB(); + } + + $taskIds = array_filter($taskIds); + if (!empty($taskIds) && ($previousStatus === self::STATUS_APPROVED || $previousStatus === self::STATUS_SEALED)) { + // EN: Recompute effective duration on related tasks after removing approved times. + // FR: Recalcule la durée effective des tâches concernées après suppression des temps approuvés. + if ($this->updateTasksDurationEffective($taskIds) < 0) { + $this->db->rollback(); + return -1; + } + } + + $this->status = self::STATUS_DRAFT; + $this->tms = $now; + $this->date_validation = null; + + if (!$this->createAgendaEvent($user, 'TSWK_REOPEN', 'TimesheetWeekAgendaReopened', array($this->ref))) { + $this->db->rollback(); + return -1; + } + + $this->db->commit(); + + return 1; + } + + /** /** * Approve * @param User $user diff --git a/core/modules/modTimesheetWeek.class.php b/core/modules/modTimesheetWeek.class.php index a3a4900..3795b57 100644 --- a/core/modules/modTimesheetWeek.class.php +++ b/core/modules/modTimesheetWeek.class.php @@ -112,7 +112,7 @@ public function __construct($db) } // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '1.6.0'; +$this->version = '1.6.1'; // Url to the file with your last numberversion of this module $this->url_last_version = 'https://moduleversion.lesmetiersdubatiment.fr/ver.php?m=timesheetweek'; diff --git a/lib/timesheetweek_pdf.lib.php b/lib/timesheetweek_pdf.lib.php index df41bc6..a5fd6af 100644 --- a/lib/timesheetweek_pdf.lib.php +++ b/lib/timesheetweek_pdf.lib.php @@ -94,7 +94,7 @@ function tw_generate_timesheet_pdf_to_temp(TimesheetWeek $sheet, Conf $conf, Tra } return array('success' => true, 'path' => $tempPath); -} + } /** * EN: Normalise a value before sending it to TCPDF by decoding HTML entities and applying the output charset. @@ -852,7 +852,7 @@ function tw_collect_summary_data($db, array $timesheetIds, User $user, $permRead } $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, t.status, u.lastname, u.firstname, u.weeklyhours, uv.lastname as validator_lastname, uv.firstname as validator_firstname"; + $sql = "SELECT t.rowid, t.entity, t.year, t.week, t.total_hours, t.overtime_hours, t.contract, 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, t.status, 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"; @@ -883,7 +883,10 @@ function tw_collect_summary_data($db, array $timesheetIds, User $user, $permRead $weekEnd = clone $weekStart; $weekEnd->modify('+6 days'); - $contractHours = (float) $row->weeklyhours; + $contractHours = (float) $row->contract; + if ($contractHours <= 0 && !empty($row->weeklyhours)) { + $contractHours = (float) $row->weeklyhours; + } if ($contractHours <= 0) { $contractHours = 35.0; } diff --git a/sql/llx_timesheet_week.sql b/sql/llx_timesheet_week.sql index 48fedc9..0e3915a 100644 --- a/sql/llx_timesheet_week.sql +++ b/sql/llx_timesheet_week.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS llx_timesheet_week ( fk_user_valid INT DEFAULT NULL, total_hours DOUBLE(24,8) NOT NULL DEFAULT 0, overtime_hours DOUBLE(24,8) NOT NULL DEFAULT 0, + contract DOUBLE(24,8) DEFAULT NULL, zone1_count SMALLINT NOT NULL DEFAULT 0, zone2_count SMALLINT NOT NULL DEFAULT 0, zone3_count SMALLINT NOT NULL DEFAULT 0, diff --git a/sql/update_all.sql b/sql/update_all.sql index bf0d64e..fd09ac6 100644 --- a/sql/update_all.sql +++ b/sql/update_all.sql @@ -14,3 +14,20 @@ SET @tsw_add_model_pdf := IF( PREPARE tsw_stmt FROM @tsw_add_model_pdf; EXECUTE tsw_stmt; DEALLOCATE PREPARE tsw_stmt; + +-- EN: Add the contract hours column to existing tables when missing. +SET @tsw_has_contract := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'llx_timesheet_week' + AND COLUMN_NAME = 'contract' +); +SET @tsw_add_contract := IF( + @tsw_has_contract = 0, + 'ALTER TABLE llx_timesheet_week ADD COLUMN contract DOUBLE(24,8) DEFAULT NULL AFTER overtime_hours', + 'SELECT 1' +); +PREPARE tsw_contract_stmt FROM @tsw_add_contract; +EXECUTE tsw_contract_stmt; +DEALLOCATE PREPARE tsw_contract_stmt; diff --git a/timesheetweek_card.php b/timesheetweek_card.php index 6941e30..baaf1b7 100644 --- a/timesheetweek_card.php +++ b/timesheetweek_card.php @@ -109,13 +109,13 @@ function tw_translate_error($errorKey, $langs) } /** - * EN: Format day totals by reusing Dolibarr price helpers to respect locale settings. - * FR: Formate les totaux en jours en réutilisant les aides de prix Dolibarr pour respecter les paramètres régionaux. - * - * @param float $value Day quantity to format / Quantité de jours à formater - * @param Translate $langs Translator instance / Instance de traduction - * @return string Formatted value / Valeur formatée - */ +* EN: Format day totals by reusing Dolibarr price helpers to respect locale settings. +* FR: Formate les totaux en jours en réutilisant les aides de prix Dolibarr pour respecter les paramètres régionaux. +* +* @param float $value Day quantity to format / Quantité de jours à formater +* @param Translate $langs Translator instance / Instance de traduction +* @return string Formatted value / Valeur formatée +*/ function tw_format_days($value, Translate $langs) { global $conf; @@ -128,13 +128,13 @@ function tw_format_days($value, Translate $langs) } /** - * EN: Fetch the timesheet employee and detect the daily rate flag with caching. - * FR: Récupère le salarié de la feuille et détecte le forfait jour avec mise en cache. - * - * @param DoliDB $db Database handler / Gestionnaire de base de données - * @param int $userId Employee identifier / Identifiant du salarié - * @return array ['user' => ?User, 'is_daily_rate' => bool] - */ +* EN: Fetch the timesheet employee and detect the daily rate flag with caching. +* FR: Récupère le salarié de la feuille et détecte le forfait jour avec mise en cache. +* +* @param DoliDB $db Database handler / Gestionnaire de base de données +* @param int $userId Employee identifier / Identifiant du salarié +* @return array ['user' => ?User, 'is_daily_rate' => bool] +*/ function tw_get_employee_with_daily_rate(DoliDB $db, $userId) { static $cache = array(); @@ -157,12 +157,12 @@ function tw_get_employee_with_daily_rate(DoliDB $db, $userId) } /** - * EN: Retrieve the list of activated PDF models for the module with entity scoping. - * FR: Récupère la liste des modèles PDF activés pour le module en respectant l'entité. - * - * @param DoliDB $db Database handler / Gestionnaire de base de données - * @return array Enabled models keyed by code / Modèles actifs indexés par code - */ +* EN: Retrieve the list of activated PDF models for the module with entity scoping. +* FR: Récupère la liste des modèles PDF activés pour le module en respectant l'entité. +* +* @param DoliDB $db Database handler / Gestionnaire de base de données +* @return array Enabled models keyed by code / Modèles actifs indexés par code +*/ function tw_get_enabled_pdf_models(DoliDB $db) { // EN: Ask the module manager for the enabled templates of TimesheetWeek. @@ -203,12 +203,12 @@ function tw_get_enabled_pdf_models(DoliDB $db) } /** - * EN: Return the hour equivalents for each daily rate code (adds quarter-day when enabled). - * FR: Retourne les équivalences en heures pour chaque code forfait (ajoute le quart de jour si activé). - * - * @param bool $useQuarterDayDailyContract Flag for quarter-day support / Drapeau d'activation du quart de jour - * @return array Hour mapping by code / Correspondance heures par code - */ +* EN: Return the hour equivalents for each daily rate code (adds quarter-day when enabled). +* FR: Retourne les équivalences en heures pour chaque code forfait (ajoute le quart de jour si activé). +* +* @param bool $useQuarterDayDailyContract Flag for quarter-day support / Drapeau d'activation du quart de jour +* @return array Hour mapping by code / Correspondance heures par code +*/ function tw_get_daily_rate_hours_map($useQuarterDayDailyContract) { $map = array( @@ -1299,8 +1299,8 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( } echo ''; - echo ''; - echo ''; + echo ''; + echo ''; // EN: Load the employee once to reuse the daily rate flag across the header and grid. // FR: Charge le salarié une seule fois pour réutiliser le flag forfait jour dans l'entête et la grille. @@ -1308,47 +1308,47 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( $timesheetEmployee = $employeeInfoDisplay['user']; $isDailyRateEmployee = $employeeInfoDisplay['is_daily_rate']; -// Right block (Totaux en entête) - $contractedHoursDisp = 35.0; - if ($timesheetEmployee instanceof User) { - $contractedHoursDisp = !empty($timesheetEmployee->weeklyhours) ? (float) $timesheetEmployee->weeklyhours : 35.0; - } -$th = (float) $object->total_hours; -$ot = (float) $object->overtime_hours; -if ($th <= 0) { -$sqlSum = "SELECT SUM(hours) as sh FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE fk_timesheet_week=".(int) $object->id; -// EN: Respect entity boundaries when recomputing totals lazily. -// FR: Respecte les frontières d'entité lors du recalcul paresseux des totaux. -$sqlSum .= " AND entity IN (".getEntity('timesheetweek').")"; -$resSum = $db->query($sqlSum); -if ($resSum) { -$o = $db->fetch_object($resSum); -$th = (float) $o->sh; -$ot = max(0.0, $th - $contractedHoursDisp); -} -} -$displayedTotal = $th; -$displayedTotalLabel = $langs->trans("TotalHours"); -$headerTotalClass = 'header-total-hours header-total-main'; -if ($isDailyRateEmployee) { - // EN: Convert stored hours into days for header display when the employee is forfait jour. - // FR: Convertit les heures enregistrées en jours pour l'affichage d'entête lorsque le salarié est au forfait jour. - $displayedTotal = ($th > 0 ? ($th / 8.0) : 0.0); - $displayedTotalLabel = $langs->trans("TotalDays"); - $headerTotalClass = 'header-total-days header-total-main'; -} -echo '
'; -echo ''; -echo ''; -echo ''; -echo ''; -if ($isDailyRateEmployee) { -echo ''; + // Right block (Totaux en entête) + $contractedHoursDisp = ($object->contract !== null ? (float) $object->contract : 35.0); + if ($contractedHoursDisp <= 0 && $timesheetEmployee instanceof User) { + $contractedHoursDisp = !empty($timesheetEmployee->weeklyhours) ? (float) $timesheetEmployee->weeklyhours : 35.0; + } + $th = (float) $object->total_hours; + $ot = (float) $object->overtime_hours; + if ($th <= 0) { + $sqlSum = "SELECT SUM(hours) as sh FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE fk_timesheet_week=".(int) $object->id; + // EN: Respect entity boundaries when recomputing totals lazily. + // FR: Respecte les frontières d'entité lors du recalcul paresseux des totaux. + $sqlSum .= " AND entity IN (".getEntity('timesheetweek').")"; + $resSum = $db->query($sqlSum); + if ($resSum) { + $o = $db->fetch_object($resSum); + $th = (float) $o->sh; + $ot = max(0.0, $th - $contractedHoursDisp); + } + } + $displayedTotal = $th; + $displayedTotalLabel = $langs->trans("TotalHours"); + $headerTotalClass = 'header-total-hours header-total-main'; + if ($isDailyRateEmployee) { + // EN: Convert stored hours into days for header display when the employee is forfait jour. + // FR: Convertit les heures enregistrées en jours pour l'affichage d'entête lorsque le salarié est au forfait jour. + $displayedTotal = ($th > 0 ? ($th / 8.0) : 0.0); + $displayedTotalLabel = $langs->trans("TotalDays"); + $headerTotalClass = 'header-total-days header-total-main'; + } + echo '
'; + echo '
'.$langs->trans("DateCreation").''.dol_print_date($object->date_creation, 'dayhour').'
'.$langs->trans("LastModification").''.dol_print_date($object->tms, 'dayhour').'
'.$langs->trans("DateValidation").''.dol_print_date($object->date_validation, 'dayhour').'
'.$displayedTotalLabel.''.tw_format_days($displayedTotal, $langs).'
'; + echo ''; + echo ''; + echo ''; + if ($isDailyRateEmployee) { + echo ''; } else { -echo ''; -echo ''; -} -echo '
'.$langs->trans("DateCreation").''.dol_print_date($object->date_creation, 'dayhour').'
'.$langs->trans("LastModification").''.dol_print_date($object->tms, 'dayhour').'
'.$langs->trans("DateValidation").''.dol_print_date($object->date_validation, 'dayhour').'
'.$displayedTotalLabel.''.tw_format_days($displayedTotal, $langs).'
'.$displayedTotalLabel.''.formatHours($displayedTotal).'
'.$langs->trans("Overtime").' ('.formatHours($contractedHoursDisp).')'.formatHours($ot).'
'; + echo ''.$displayedTotalLabel.''.formatHours($displayedTotal).''; + echo ''.$langs->trans("Overtime").' ('.formatHours($contractedHoursDisp).')'.formatHours($ot).''; + } + echo ''; echo '
'; echo ''; // fichecenter