Skip to content

Commit 7de4881

Browse files
authored
Merge pull request #66 from mapiolca/1.2.0
1.2.0
2 parents 06abca2 + 2a2b9d7 commit 7de4881

File tree

8 files changed

+1302
-1030
lines changed

8 files changed

+1302
-1030
lines changed

ChangeLog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
22

3+
## 1.2.0
4+
- Ajoute le support des contrats « Cadre au forfait jour » avec sélecteurs Journée/Matin/Après-midi et conversion automatique des durées en base. / Adds support for "daily rate" contracts with Full day/Morning/Afternoon selectors and automatic duration conversion.
5+
- Crée l'extrafield salarié « Contrat forfait jour » et stocke la sélection correspondante dans la colonne dédiée. / Creates the employee extrafield "Daily rate contract" and stores the related selection in the dedicated column.
6+
- Complète les traductions « LastModification » et « TotalDays » pour harmoniser l'affichage du forfait jour. / Completes the "LastModification" and "TotalDays" translations to harmonise the daily rate display.
7+
38
## 1.1.1
49
- Mise à "plat" des permissions pour régler un problème d'affichage des PDF. / "Flattening" permissions to fix a PDF display issue.
510

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ TimesheetWeek ajoute une gestion hebdomadaire des feuilles de temps fidèle à l
99
- Statut « Scellée » pour verrouiller les feuilles approuvées et empêcher toute modification ultérieure, avec les permissions associées.
1010
- Redirection automatique vers la feuille existante en cas de tentative de doublon afin d'éviter les saisies multiples.
1111
- Suivi des compteurs hebdomadaires de zones et de paniers directement sur les feuilles et recalcul automatique à chaque enregistrement.
12+
- 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.
1213
- Affichage des compteurs dans la liste hebdomadaire et ajout du libellé « Zone » sur chaque sélecteur quotidien pour clarifier la saisie.
1314
- Ligne de total en bas de la liste hebdomadaire pour additionner heures, zones, paniers et afficher la colonne de date de validation.
1415
- Création rapide d'une feuille d'heures via le raccourci « Ajouter » du menu supérieur.
@@ -48,6 +49,7 @@ TimesheetWeek delivers weekly timesheet management that follows Dolibarr design
4849
- Statut « Scellée » (Sealed status) to lock approved timesheets together with the related permissions.
4950
- Automatic redirect to the existing timesheet when a duplicate creation is attempted.
5051
- Weekly counters for zones and meal allowances with automatic recomputation on each save.
52+
- Dedicated input for daily rate employees with Full day/Morning/Afternoon selectors that automatically convert hours.
5153
- Counter display inside the weekly list plus a « Zone » caption on each daily selector for better input guidance.
5254
- Total row at the bottom of the weekly list to sum hours, zones, meals and expose the validation date column.
5355
- Quick creation shortcut available from the top-right « Add » menu.

class/timesheetweekline.class.php

Lines changed: 151 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,169 @@
11
<?php
2-
/* Copyright (C) 2025 Pierre ARDOIN
3-
*
4-
* This program is free software; you can redistribute it and/or modify
5-
* it under the terms of the GNU General Public License.
6-
*/
2+
/* Copyright (C) 2025 Pierre Ardoin <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
717

818
/**
9-
* \file timesheetweek/class/timesheetweekline.class.php
10-
* \ingroup timesheetweek
11-
* \brief TimesheetWeekLine class file
12-
*/
19+
* \file timesheetweek/class/timesheetweekline.class.php
20+
* \ingroup timesheetweek
21+
* \brief TimesheetWeekLine class file
22+
*/
1323

1424
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobjectline.class.php';
1525

1626
class TimesheetWeekLine extends CommonObjectLine
1727
{
18-
public $element = 'timesheetweekline';
19-
public $table_element = 'timesheet_week_line';
20-
public $fk_element = 'fk_timesheet_week';
21-
public $parent_element = 'timesheetweek';
28+
public $element = 'timesheetweekline';
29+
public $table_element = 'timesheet_week_line';
30+
public $fk_element = 'fk_timesheet_week';
31+
public $parent_element = 'timesheetweek';
2232

23-
// EN: Entity identifier bound to the line.
24-
// FR: Identifiant d'entité lié à la ligne.
25-
public $entity;
26-
public $fk_timesheet_week;
27-
public $fk_task;
28-
public $day_date;
29-
public $hours;
30-
public $zone;
31-
public $meal;
33+
// EN: Entity identifier bound to the line.
34+
// FR: Identifiant d'entité lié à la ligne.
35+
public $entity;
36+
public $fk_timesheet_week;
37+
public $fk_task;
38+
public $day_date;
39+
public $hours;
40+
// EN: Stores the selected daily rate value linked to forfait-jour entries.
41+
// FR: Stocke la valeur de forfait-jour sélectionnée pour les saisies concernées.
42+
public $daily_rate;
43+
public $zone;
44+
public $meal;
3245

33-
/**
34-
* Fetches a timesheet line by its rowid.
35-
* Récupère une ligne de feuille de temps via son rowid.
36-
*
37-
* @param int $id Row identifier / Identifiant de ligne
38-
* @return int >0 success, <=0 error / >0 succès, <=0 erreur
39-
*/
40-
public function fetch($id)
41-
{
42-
if (empty($id)) {
43-
return 0;
44-
}
46+
/**
47+
* Fetches a timesheet line by its rowid.
48+
* Récupère une ligne de feuille de temps via son rowid.
49+
*
50+
* @param int $id Row identifier / Identifiant de ligne
51+
* @return int >0 success, <=0 error / >0 succès, <=0 erreur
52+
*/
53+
public function fetch($id)
54+
{
55+
if (empty($id)) {
56+
return 0;
57+
}
4558

46-
// Build the query for the line / Construit la requête pour la ligne
47-
$sql = "SELECT rowid, entity, fk_timesheet_week, fk_task, day_date, hours, zone, meal";
48-
$sql .= " FROM ".MAIN_DB_PREFIX."timesheet_week_line";
49-
$sql .= " WHERE rowid=".(int) $id;
50-
// EN: Respect module entity permissions during line fetch.
51-
// FR: Respecte les permissions d'entité du module lors du chargement de la ligne.
52-
$sql .= " AND entity IN (".getEntity('timesheetweek').")";
59+
// Build the query for the line / Construit la requête pour la ligne
60+
$sql = "SELECT rowid, entity, fk_timesheet_week, fk_task, day_date, hours, daily_rate, zone, meal";
61+
$sql .= " FROM ".MAIN_DB_PREFIX."timesheet_week_line";
62+
$sql .= " WHERE rowid=".(int) $id;
63+
// EN: Respect module entity permissions during line fetch.
64+
// FR: Respecte les permissions d'entité du module lors du chargement de la ligne.
65+
$sql .= " AND entity IN (".getEntity('timesheetweek').")";
5366

54-
$resql = $this->db->query($sql);
55-
if (!$resql) {
56-
$this->error = $this->db->lasterror();
57-
return -1;
58-
}
67+
$resql = $this->db->query($sql);
68+
if (!$resql) {
69+
$this->error = $this->db->lasterror();
70+
return -1;
71+
}
5972

60-
$obj = $this->db->fetch_object($resql);
61-
$this->db->free($resql);
62-
if (!$obj) {
63-
return 0;
64-
}
73+
$obj = $this->db->fetch_object($resql);
74+
$this->db->free($resql);
75+
if (!$obj) {
76+
return 0;
77+
}
6578

66-
// Map database values to object / Mappe les valeurs de la base vers l'objet
67-
$this->id = (int) $obj->rowid;
68-
$this->rowid = (int) $obj->rowid;
69-
$this->entity = (int) $obj->entity;
70-
$this->fk_timesheet_week = (int) $obj->fk_timesheet_week;
71-
$this->fk_task = (int) $obj->fk_task;
72-
$this->day_date = $obj->day_date;
73-
$this->hours = (float) $obj->hours;
74-
$this->zone = (int) $obj->zone;
75-
$this->meal = (int) $obj->meal;
79+
// Map database values to object / Mappe les valeurs de la base vers l'objet
80+
$this->id = (int) $obj->rowid;
81+
$this->rowid = (int) $obj->rowid;
82+
$this->entity = (int) $obj->entity;
83+
$this->fk_timesheet_week = (int) $obj->fk_timesheet_week;
84+
$this->fk_task = (int) $obj->fk_task;
85+
$this->day_date = $obj->day_date;
86+
$this->hours = (float) $obj->hours;
87+
$this->daily_rate = (int) $obj->daily_rate;
88+
$this->zone = (int) $obj->zone;
89+
$this->meal = (int) $obj->meal;
7690

77-
return 1;
78-
}
91+
return 1;
92+
}
7993

80-
/**
81-
* Crée ou met à jour la ligne si elle existe déjà
82-
*/
83-
public function save($user)
84-
{
85-
// EN: Resolve the entity from the parent sheet when not provided.
86-
// FR: Récupère l'entité depuis la feuille parente lorsqu'elle n'est pas fournie.
87-
if (empty($this->entity) && !empty($this->fk_timesheet_week)) {
88-
$sqlEntity = "SELECT entity FROM ".MAIN_DB_PREFIX."timesheet_week WHERE rowid=".(int) $this->fk_timesheet_week;
89-
$sqlEntity .= " AND entity IN (".getEntity('timesheetweek').")";
90-
$resEntity = $this->db->query($sqlEntity);
91-
if ($resEntity) {
92-
$objEntity = $this->db->fetch_object($resEntity);
93-
if ($objEntity) {
94-
$this->entity = (int) $objEntity->entity;
95-
}
96-
}
97-
}
98-
if (empty($this->entity)) {
99-
global $conf;
100-
// EN: Fall back to the current context entity as a last resort.
101-
// FR: Revient en dernier recours à l'entité du contexte courant.
102-
$this->entity = isset($conf->entity) ? (int) $conf->entity : 1;
103-
}
94+
/**
95+
* Saves the line or updates it when already stored.
96+
* Crée ou met à jour la ligne si elle existe déjà.
97+
*/
98+
public function save($user)
99+
{
100+
// EN: Resolve the entity from the parent sheet when not provided.
101+
// FR: Récupère l'entité depuis la feuille parente lorsqu'elle n'est pas fournie.
102+
if (empty($this->entity) && !empty($this->fk_timesheet_week)) {
103+
$sqlEntity = "SELECT entity FROM ".MAIN_DB_PREFIX."timesheet_week WHERE rowid=".(int) $this->fk_timesheet_week;
104+
$sqlEntity .= " AND entity IN (".getEntity('timesheetweek').")";
105+
$resEntity = $this->db->query($sqlEntity);
106+
if ($resEntity) {
107+
$objEntity = $this->db->fetch_object($resEntity);
108+
if ($objEntity) {
109+
$this->entity = (int) $objEntity->entity;
110+
}
111+
}
112+
}
113+
if (empty($this->entity)) {
114+
global $conf;
115+
// EN: Fall back to the current context entity as a last resort.
116+
// FR: Revient en dernier recours à l'entité du contexte courant.
117+
$this->entity = isset($conf->entity) ? (int) $conf->entity : 1;
118+
}
119+
120+
// EN: Normalize the stored daily rate to avoid null values reaching SQL.
121+
// FR: Normalise la valeur de forfait-jour pour éviter d'envoyer des NULL en SQL.
122+
if ($this->daily_rate === null) {
123+
$this->daily_rate = 0;
124+
}
104125

105-
// Vérifie si une ligne existe déjà pour cette tâche et ce jour
106-
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."timesheet_week_line";
107-
$sql .= " WHERE fk_timesheet_week = ".((int)$this->fk_timesheet_week);
108-
$sql .= " AND fk_task = ".((int)$this->fk_task);
109-
$sql .= " AND day_date = '".$this->db->escape($this->day_date)."'";
110-
// EN: Keep lookups limited to lines inside allowed entities.
111-
// FR: Limite les recherches aux lignes situées dans les entités autorisées.
112-
$sql .= " AND entity IN (".getEntity('timesheetweek').")";
126+
// EN: Check whether a line already exists for this task and day.
127+
// FR: Vérifie si une ligne existe déjà pour cette tâche et ce jour.
128+
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."timesheet_week_line";
129+
$sql .= " WHERE fk_timesheet_week = ".((int)$this->fk_timesheet_week);
130+
$sql .= " AND fk_task = ".((int)$this->fk_task);
131+
$sql .= " AND day_date = '".$this->db->escape($this->day_date)."'";
132+
// EN: Keep lookups limited to lines inside allowed entities.
133+
// FR: Limite les recherches aux lignes situées dans les entités autorisées.
134+
$sql .= " AND entity IN (".getEntity('timesheetweek').")";
113135

114-
$resql = $this->db->query($sql);
115-
if ($resql && $this->db->num_rows($resql) > 0) {
116-
// ---- UPDATE ----
117-
$obj = $this->db->fetch_object($resql);
118-
$sqlu = "UPDATE ".MAIN_DB_PREFIX."timesheet_week_line SET ";
119-
$sqlu .= " hours = ".((float)$this->hours).",";
120-
$sqlu .= " zone = ".((int)$this->zone).",";
121-
$sqlu .= " meal = ".((int)$this->meal);
122-
$sqlu .= " WHERE rowid = ".((int)$obj->rowid);
123-
// EN: Ensure updates stay within the permitted entity scope.
124-
// FR: Assure que les mises à jour restent dans le périmètre d'entité autorisé.
125-
$sqlu .= " AND entity IN (".getEntity('timesheetweek').")";
126-
return $this->db->query($sqlu) ? 1 : -1;
127-
}
128-
else {
129-
// ---- INSERT ----
130-
// EN: Persist the entity alongside the usual line fields for consistency.
131-
// FR: Enregistre l'entité avec les champs habituels de la ligne pour rester cohérent.
132-
$sqli = "INSERT INTO ".MAIN_DB_PREFIX."timesheet_week_line(";
133-
$sqli .= " entity, fk_timesheet_week, fk_task, day_date, hours, zone, meal)";
134-
$sqli .= " VALUES(";
135-
$sqli .= (int)$this->entity.",";
136-
$sqli .= (int)$this->fk_timesheet_week.",";
137-
$sqli .= (int)$this->fk_task.",";
138-
$sqli .= "'".$this->db->escape($this->day_date)."',";
139-
$sqli .= (float)$this->hours.",";
140-
$sqli .= (int)$this->zone.",";
141-
$sqli .= (int)$this->meal.")";
142-
return $this->db->query($sqli) ? 1 : -1;
143-
}
144-
}
145-
}
136+
$resql = $this->db->query($sql);
137+
if ($resql && $this->db->num_rows($resql) > 0) {
138+
// ---- UPDATE ----
139+
$obj = $this->db->fetch_object($resql);
140+
$sqlu = "UPDATE ".MAIN_DB_PREFIX."timesheet_week_line SET ";
141+
$sqlu .= " hours = ".((float)$this->hours).",";
142+
$sqlu .= " daily_rate = ".((int)$this->daily_rate).",";
143+
$sqlu .= " zone = ".((int)$this->zone).",";
144+
$sqlu .= " meal = ".((int)$this->meal);
145+
$sqlu .= " WHERE rowid = ".((int)$obj->rowid);
146+
// EN: Ensure updates stay within the permitted entity scope.
147+
// FR: Assure que les mises à jour restent dans le périmètre d'entité autorisé.
148+
$sqlu .= " AND entity IN (".getEntity('timesheetweek').")";
149+
return $this->db->query($sqlu) ? 1 : -1;
150+
}
151+
else {
152+
// ---- INSERT ----
153+
// EN: Persist the entity alongside the usual line fields for consistency.
154+
// FR: Enregistre l'entité avec les champs habituels de la ligne pour rester cohérent.
155+
$sqli = "INSERT INTO ".MAIN_DB_PREFIX."timesheet_week_line(";
156+
$sqli .= " entity, fk_timesheet_week, fk_task, day_date, hours, daily_rate, zone, meal)";
157+
$sqli .= " VALUES(";
158+
$sqli .= (int)$this->entity.",";
159+
$sqli .= (int)$this->fk_timesheet_week.",";
160+
$sqli .= (int)$this->fk_task.",";
161+
$sqli .= "'".$this->db->escape($this->day_date)."',";
162+
$sqli .= (float)$this->hours.",";
163+
$sqli .= (int)$this->daily_rate.",";
164+
$sqli .= (int)$this->zone.",";
165+
$sqli .= (int)$this->meal.")";
166+
return $this->db->query($sqli) ? 1 : -1;
167+
}
168+
}
169+
}

core/modules/modTimesheetWeek.class.php

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ public function __construct($db)
8080
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@timesheetweek'
8181

8282
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
83-
$this->version = '1.1.1'; // EN: "Flattening" permissions to fix a PDF display issue.
84-
// FR: Mise à "plat" des permissions pour régler un problème d'affichage des PDF.
83+
$this->version = '1.2.0'; // EN: Adds forfait-jour daily rate selectors with automatic hour conversion.
84+
// FR: Ajoute les sélecteurs forfait jour Journée/Matin/Après-midi avec conversion automatique des heures.
8585
// Url to the file with your last numberversion of this module
8686
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
8787

@@ -740,14 +740,36 @@ public function init($options = '')
740740
}
741741

742742
// Create extrafields during init
743-
//include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
744-
//$extrafields = new ExtraFields($this->db);
745-
//$result0=$extrafields->addExtraField('timesheetweek_separator1', "Separator 1", 'separator', 1, 0, 'thirdparty', 0, 0, '', array('options'=>array(1=>1)), 1, '', 1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
746-
//$result1=$extrafields->addExtraField('timesheetweek_myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
747-
//$result2=$extrafields->addExtraField('timesheetweek_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
748-
//$result3=$extrafields->addExtraField('timesheetweek_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
749-
//$result4=$extrafields->addExtraField('timesheetweek_myattr4', "New Attr 4 label", 'select', 1, 3, 'thirdparty', 0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1,'', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
750-
//$result5=$extrafields->addExtraField('timesheetweek_myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
743+
dol_include_once('/core/class/extrafields.class.php');
744+
$extrafields = new ExtraFields($this->db);
745+
$extrafields->fetch_name_optionals_label('user');
746+
if (empty($extrafields->attributes['user']['label']['lmdb_daily_rate'])) {
747+
// EN: Register the daily rate toggle on employees when the module is activated.
748+
// FR: Enregistre l'option de forfait jour sur les salariés lors de l'activation du module.
749+
$extrafields->addExtraField('lmdb_daily_rate', 'TimesheetWeekDailyRateLabel', 'boolean', 100, '', 'user', 0, 0, '', '', 0, '', '0', '', '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")', 0, 0);
750+
}
751+
752+
// EN: Ensure existing installations receive the daily_rate column for time entries.
753+
// FR: Garantit que les installations existantes reçoivent la colonne daily_rate pour les lignes de temps.
754+
$sqlCheckDailyRate = "SHOW COLUMNS FROM ".$this->db->prefix()."timesheet_week_line LIKE 'daily_rate'";
755+
$resqlCheckDailyRate = $this->db->query($sqlCheckDailyRate);
756+
if (!$resqlCheckDailyRate) {
757+
// EN: Abort activation when the structure verification query fails.
758+
// FR: Interrompt l'activation si la requête de vérification de structure échoue.
759+
$this->error = $this->db->lasterror();
760+
return -1;
761+
}
762+
$hasDailyRateColumn = (bool) $this->db->num_rows($resqlCheckDailyRate);
763+
$this->db->free($resqlCheckDailyRate);
764+
if (!$hasDailyRateColumn) {
765+
$sqlAdd = "ALTER TABLE ".MAIN_DB_PREFIX."timesheet_week_line ADD COLUMN daily_rate INT NOT NULL DEFAULT 0";
766+
if (!$this->db->query($sqlAdd)) {
767+
// EN: Stop activation when adding the column fails to keep database consistent.
768+
// FR: Stoppe l'activation si l'ajout de la colonne échoue pour conserver une base cohérente.
769+
$this->error = $this->db->lasterror();
770+
return -1;
771+
}
772+
}
751773

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

0 commit comments

Comments
 (0)