Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)

## 1.2.0
- 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.
- 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.
- 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.

## 1.1.1
- Mise à "plat" des permissions pour régler un problème d'affichage des PDF. / "Flattening" permissions to fix a PDF display issue.

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ TimesheetWeek ajoute une gestion hebdomadaire des feuilles de temps fidèle à l
- Statut « Scellée » pour verrouiller les feuilles approuvées et empêcher toute modification ultérieure, avec les permissions associées.
- Redirection automatique vers la feuille existante en cas de tentative de doublon afin d'éviter les saisies multiples.
- Suivi des compteurs hebdomadaires de zones et de paniers directement sur les feuilles et recalcul automatique à chaque enregistrement.
- 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.
- Affichage des compteurs dans la liste hebdomadaire et ajout du libellé « Zone » sur chaque sélecteur quotidien pour clarifier la saisie.
- 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.
Expand Down Expand Up @@ -48,6 +49,7 @@ TimesheetWeek delivers weekly timesheet management that follows Dolibarr design
- Statut « Scellée » (Sealed status) to lock approved timesheets together with the related permissions.
- Automatic redirect to the existing timesheet when a duplicate creation is attempted.
- Weekly counters for zones and meal allowances with automatic recomputation on each save.
- Dedicated input for daily rate employees with Full day/Morning/Afternoon selectors that automatically convert hours.
- Counter display inside the weekly list plus a « Zone » caption on each daily selector for better input guidance.
- 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.
Expand Down
278 changes: 151 additions & 127 deletions class/timesheetweekline.class.php
Original file line number Diff line number Diff line change
@@ -1,145 +1,169 @@
<?php
/* Copyright (C) 2025 Pierre ARDOIN
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License.
*/
/* Copyright (C) 2025 Pierre Ardoin <[email protected]>
*
* 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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
* \file timesheetweek/class/timesheetweekline.class.php
* \ingroup timesheetweek
* \brief TimesheetWeekLine class file
*/
* \file timesheetweek/class/timesheetweekline.class.php
* \ingroup timesheetweek
* \brief TimesheetWeekLine class file
*/

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

class TimesheetWeekLine extends CommonObjectLine
{
public $element = 'timesheetweekline';
public $table_element = 'timesheet_week_line';
public $fk_element = 'fk_timesheet_week';
public $parent_element = 'timesheetweek';
public $element = 'timesheetweekline';
public $table_element = 'timesheet_week_line';
public $fk_element = 'fk_timesheet_week';
public $parent_element = 'timesheetweek';

// EN: Entity identifier bound to the line.
// FR: Identifiant d'entité lié à la ligne.
public $entity;
public $fk_timesheet_week;
public $fk_task;
public $day_date;
public $hours;
public $zone;
public $meal;
// EN: Entity identifier bound to the line.
// FR: Identifiant d'entité lié à la ligne.
public $entity;
public $fk_timesheet_week;
public $fk_task;
public $day_date;
public $hours;
// EN: Stores the selected daily rate value linked to forfait-jour entries.
// FR: Stocke la valeur de forfait-jour sélectionnée pour les saisies concernées.
public $daily_rate;
public $zone;
public $meal;

/**
* Fetches a timesheet line by its rowid.
* Récupère une ligne de feuille de temps via son rowid.
*
* @param int $id Row identifier / Identifiant de ligne
* @return int >0 success, <=0 error / >0 succès, <=0 erreur
*/
public function fetch($id)
{
if (empty($id)) {
return 0;
}
/**
* Fetches a timesheet line by its rowid.
* Récupère une ligne de feuille de temps via son rowid.
*
* @param int $id Row identifier / Identifiant de ligne
* @return int >0 success, <=0 error / >0 succès, <=0 erreur
*/
public function fetch($id)
{
if (empty($id)) {
return 0;
}

// Build the query for the line / Construit la requête pour la ligne
$sql = "SELECT rowid, entity, fk_timesheet_week, fk_task, day_date, hours, zone, meal";
$sql .= " FROM ".MAIN_DB_PREFIX."timesheet_week_line";
$sql .= " WHERE rowid=".(int) $id;
// EN: Respect module entity permissions during line fetch.
// FR: Respecte les permissions d'entité du module lors du chargement de la ligne.
$sql .= " AND entity IN (".getEntity('timesheetweek').")";
// Build the query for the line / Construit la requête pour la ligne
$sql = "SELECT rowid, entity, fk_timesheet_week, fk_task, day_date, hours, daily_rate, zone, meal";
$sql .= " FROM ".MAIN_DB_PREFIX."timesheet_week_line";
$sql .= " WHERE rowid=".(int) $id;
// EN: Respect module entity permissions during line fetch.
// FR: Respecte les permissions d'entité du module lors du chargement de la ligne.
$sql .= " AND entity IN (".getEntity('timesheetweek').")";

$resql = $this->db->query($sql);
if (!$resql) {
$this->error = $this->db->lasterror();
return -1;
}
$resql = $this->db->query($sql);
if (!$resql) {
$this->error = $this->db->lasterror();
return -1;
}

$obj = $this->db->fetch_object($resql);
$this->db->free($resql);
if (!$obj) {
return 0;
}
$obj = $this->db->fetch_object($resql);
$this->db->free($resql);
if (!$obj) {
return 0;
}

// Map database values to object / Mappe les valeurs de la base vers l'objet
$this->id = (int) $obj->rowid;
$this->rowid = (int) $obj->rowid;
$this->entity = (int) $obj->entity;
$this->fk_timesheet_week = (int) $obj->fk_timesheet_week;
$this->fk_task = (int) $obj->fk_task;
$this->day_date = $obj->day_date;
$this->hours = (float) $obj->hours;
$this->zone = (int) $obj->zone;
$this->meal = (int) $obj->meal;
// Map database values to object / Mappe les valeurs de la base vers l'objet
$this->id = (int) $obj->rowid;
$this->rowid = (int) $obj->rowid;
$this->entity = (int) $obj->entity;
$this->fk_timesheet_week = (int) $obj->fk_timesheet_week;
$this->fk_task = (int) $obj->fk_task;
$this->day_date = $obj->day_date;
$this->hours = (float) $obj->hours;
$this->daily_rate = (int) $obj->daily_rate;
$this->zone = (int) $obj->zone;
$this->meal = (int) $obj->meal;

return 1;
}
return 1;
}

/**
* Crée ou met à jour la ligne si elle existe déjà
*/
public function save($user)
{
// EN: Resolve the entity from the parent sheet when not provided.
// FR: Récupère l'entité depuis la feuille parente lorsqu'elle n'est pas fournie.
if (empty($this->entity) && !empty($this->fk_timesheet_week)) {
$sqlEntity = "SELECT entity FROM ".MAIN_DB_PREFIX."timesheet_week WHERE rowid=".(int) $this->fk_timesheet_week;
$sqlEntity .= " AND entity IN (".getEntity('timesheetweek').")";
$resEntity = $this->db->query($sqlEntity);
if ($resEntity) {
$objEntity = $this->db->fetch_object($resEntity);
if ($objEntity) {
$this->entity = (int) $objEntity->entity;
}
}
}
if (empty($this->entity)) {
global $conf;
// EN: Fall back to the current context entity as a last resort.
// FR: Revient en dernier recours à l'entité du contexte courant.
$this->entity = isset($conf->entity) ? (int) $conf->entity : 1;
}
/**
* Saves the line or updates it when already stored.
* Crée ou met à jour la ligne si elle existe déjà.
*/
public function save($user)
{
// EN: Resolve the entity from the parent sheet when not provided.
// FR: Récupère l'entité depuis la feuille parente lorsqu'elle n'est pas fournie.
if (empty($this->entity) && !empty($this->fk_timesheet_week)) {
$sqlEntity = "SELECT entity FROM ".MAIN_DB_PREFIX."timesheet_week WHERE rowid=".(int) $this->fk_timesheet_week;
$sqlEntity .= " AND entity IN (".getEntity('timesheetweek').")";
$resEntity = $this->db->query($sqlEntity);
if ($resEntity) {
$objEntity = $this->db->fetch_object($resEntity);
if ($objEntity) {
$this->entity = (int) $objEntity->entity;
}
}
}
if (empty($this->entity)) {
global $conf;
// EN: Fall back to the current context entity as a last resort.
// FR: Revient en dernier recours à l'entité du contexte courant.
$this->entity = isset($conf->entity) ? (int) $conf->entity : 1;
}

// EN: Normalize the stored daily rate to avoid null values reaching SQL.
// FR: Normalise la valeur de forfait-jour pour éviter d'envoyer des NULL en SQL.
if ($this->daily_rate === null) {
$this->daily_rate = 0;
}

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

$resql = $this->db->query($sql);
if ($resql && $this->db->num_rows($resql) > 0) {
// ---- UPDATE ----
$obj = $this->db->fetch_object($resql);
$sqlu = "UPDATE ".MAIN_DB_PREFIX."timesheet_week_line SET ";
$sqlu .= " hours = ".((float)$this->hours).",";
$sqlu .= " zone = ".((int)$this->zone).",";
$sqlu .= " meal = ".((int)$this->meal);
$sqlu .= " WHERE rowid = ".((int)$obj->rowid);
// EN: Ensure updates stay within the permitted entity scope.
// FR: Assure que les mises à jour restent dans le périmètre d'entité autorisé.
$sqlu .= " AND entity IN (".getEntity('timesheetweek').")";
return $this->db->query($sqlu) ? 1 : -1;
}
else {
// ---- INSERT ----
// EN: Persist the entity alongside the usual line fields for consistency.
// FR: Enregistre l'entité avec les champs habituels de la ligne pour rester cohérent.
$sqli = "INSERT INTO ".MAIN_DB_PREFIX."timesheet_week_line(";
$sqli .= " entity, fk_timesheet_week, fk_task, day_date, hours, zone, meal)";
$sqli .= " VALUES(";
$sqli .= (int)$this->entity.",";
$sqli .= (int)$this->fk_timesheet_week.",";
$sqli .= (int)$this->fk_task.",";
$sqli .= "'".$this->db->escape($this->day_date)."',";
$sqli .= (float)$this->hours.",";
$sqli .= (int)$this->zone.",";
$sqli .= (int)$this->meal.")";
return $this->db->query($sqli) ? 1 : -1;
}
}
}
$resql = $this->db->query($sql);
if ($resql && $this->db->num_rows($resql) > 0) {
// ---- UPDATE ----
$obj = $this->db->fetch_object($resql);
$sqlu = "UPDATE ".MAIN_DB_PREFIX."timesheet_week_line SET ";
$sqlu .= " hours = ".((float)$this->hours).",";
$sqlu .= " daily_rate = ".((int)$this->daily_rate).",";
$sqlu .= " zone = ".((int)$this->zone).",";
$sqlu .= " meal = ".((int)$this->meal);
$sqlu .= " WHERE rowid = ".((int)$obj->rowid);
// EN: Ensure updates stay within the permitted entity scope.
// FR: Assure que les mises à jour restent dans le périmètre d'entité autorisé.
$sqlu .= " AND entity IN (".getEntity('timesheetweek').")";
return $this->db->query($sqlu) ? 1 : -1;
}
else {
// ---- INSERT ----
// EN: Persist the entity alongside the usual line fields for consistency.
// FR: Enregistre l'entité avec les champs habituels de la ligne pour rester cohérent.
$sqli = "INSERT INTO ".MAIN_DB_PREFIX."timesheet_week_line(";
$sqli .= " entity, fk_timesheet_week, fk_task, day_date, hours, daily_rate, zone, meal)";
$sqli .= " VALUES(";
$sqli .= (int)$this->entity.",";
$sqli .= (int)$this->fk_timesheet_week.",";
$sqli .= (int)$this->fk_task.",";
$sqli .= "'".$this->db->escape($this->day_date)."',";
$sqli .= (float)$this->hours.",";
$sqli .= (int)$this->daily_rate.",";
$sqli .= (int)$this->zone.",";
$sqli .= (int)$this->meal.")";
return $this->db->query($sqli) ? 1 : -1;
}
}
}
42 changes: 32 additions & 10 deletions core/modules/modTimesheetWeek.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ public function __construct($db)
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@timesheetweek'

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

Expand Down Expand Up @@ -740,14 +740,36 @@ public function init($options = '')
}

// Create extrafields during init
//include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
//$extrafields = new ExtraFields($this->db);
//$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")');
//$result1=$extrafields->addExtraField('timesheetweek_myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
//$result2=$extrafields->addExtraField('timesheetweek_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
//$result3=$extrafields->addExtraField('timesheetweek_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
//$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")');
//$result5=$extrafields->addExtraField('timesheetweek_myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")');
dol_include_once('/core/class/extrafields.class.php');
$extrafields = new ExtraFields($this->db);
$extrafields->fetch_name_optionals_label('user');
if (empty($extrafields->attributes['user']['label']['lmdb_daily_rate'])) {
// EN: Register the daily rate toggle on employees when the module is activated.
// FR: Enregistre l'option de forfait jour sur les salariés lors de l'activation du module.
$extrafields->addExtraField('lmdb_daily_rate', 'TimesheetWeekDailyRateLabel', 'boolean', 100, '', 'user', 0, 0, '', '', 0, '', '0', '', '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")', 0, 0);
}

// EN: Ensure existing installations receive the daily_rate column for time entries.
// FR: Garantit que les installations existantes reçoivent la colonne daily_rate pour les lignes de temps.
$sqlCheckDailyRate = "SHOW COLUMNS FROM ".$this->db->prefix()."timesheet_week_line LIKE 'daily_rate'";
$resqlCheckDailyRate = $this->db->query($sqlCheckDailyRate);
if (!$resqlCheckDailyRate) {
// EN: Abort activation when the structure verification query fails.
// FR: Interrompt l'activation si la requête de vérification de structure échoue.
$this->error = $this->db->lasterror();
return -1;
}
$hasDailyRateColumn = (bool) $this->db->num_rows($resqlCheckDailyRate);
$this->db->free($resqlCheckDailyRate);
if (!$hasDailyRateColumn) {
$sqlAdd = "ALTER TABLE ".MAIN_DB_PREFIX."timesheet_week_line ADD COLUMN daily_rate INT NOT NULL DEFAULT 0";
if (!$this->db->query($sqlAdd)) {
// EN: Stop activation when adding the column fails to keep database consistent.
// FR: Stoppe l'activation si l'ajout de la colonne échoue pour conserver une base cohérente.
$this->error = $this->db->lasterror();
return -1;
}
}

// Permissions
$this->remove($options);
Expand Down
Loading