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: 4 additions & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)

## 1.5.0 (01/12/2025)
- Ajoute une action de masse pour générer un PDF unique fusionnant les feuilles sélectionnées en utilisant le modèle par défaut. / Adds a mass action to generate a single merged PDF for selected timesheets using the default template.

## 1.4.2 (01/12/2025)
- Corrige le masquage par défaut des tâches dont la progression est de 100% dans les feuilles.
/ Fixes the default hiding of tasks with 100% progress in sheets.

## 1.4.1 (26/11/2025)
- Met à jour automatiquement la durée effective des tâches après validation d'une feuille en se basant sur les temps consommés.
/ Automatically updates tasks' effective duration after approving a sheet based on consumed times.
Expand Down
4 changes: 2 additions & 2 deletions core/modules/modTimesheetWeek.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ 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.4.2';
$this->version = '1.5.0';
// Url to the file with your last numberversion of this module
$this->url_last_version = 'https://moduleversion.lesmetiersdubatiment.fr/ver.php?m=timesheetweek';

Expand Down
7 changes: 7 additions & 0 deletions langs/en_US/timesheetweek.lang
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ TimesheetWeekMassSealSuccess = %s timesheet(s) sealed.
TimesheetWeekMassActionErrors = Unable to process: %s.
TimesheetWeekMassDeleteOnlyDraft = Only draft timesheets can be deleted in bulk.
GenerateSummaryPdf = Generate summary PDF
TimesheetWeekMassMergePdf = Generate merged PDF for selected timesheets
TimesheetWeekMassMergeForbidden = You are not allowed to access timesheet %s
TimesheetWeekMassMergeGenerationFailed = Failed to generate PDF for %s
TimesheetWeekMassMergeMissingFile = Unable to find the generated PDF for %s
TimesheetWeekMassMergeNoEligible = No PDF could be generated from the selected timesheets
TimesheetWeekMassMergeError = Failed to merge the selected timesheets into a single PDF
TimesheetWeekMassMergeSuccess = Merged PDF generated successfully
TimesheetWeekSummaryNoSelection = Please select at least one timesheet to create the summary.
TimesheetWeekSummaryUnauthorizedSheet = Some selected timesheets were ignored because you are not allowed to read them.
TimesheetWeekSummaryMissingUser = The employee linked to a selected timesheet no longer exists.
Expand Down
7 changes: 7 additions & 0 deletions langs/fr_FR/timesheetweek.lang
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ TimesheetWeekMassSealSuccess = %s feuille(s) de temps scellée(s).
TimesheetWeekMassActionErrors = Impossible de traiter : %s.
TimesheetWeekMassDeleteOnlyDraft = Seules les feuilles en brouillon peuvent être supprimées en masse.
GenerateSummaryPdf = Générer le PDF de synthèse
TimesheetWeekMassMergePdf = Générer un PDF fusionné des feuilles sélectionnées
TimesheetWeekMassMergeForbidden = Vous n'avez pas l'autorisation d'accéder à la feuille %s
TimesheetWeekMassMergeGenerationFailed = Impossible de générer le PDF pour %s
TimesheetWeekMassMergeMissingFile = Impossible de trouver le PDF généré pour %s
TimesheetWeekMassMergeNoEligible = Aucun PDF n'a pu être généré depuis les feuilles sélectionnées
TimesheetWeekMassMergeError = Échec de la fusion des feuilles sélectionnées en un seul PDF
TimesheetWeekMassMergeSuccess = PDF fusionné généré avec succès
TimesheetWeekSummaryNoSelection = Merci de sélectionner au moins une feuille de temps pour créer la synthèse.
TimesheetWeekSummaryUnauthorizedSheet = Certaines feuilles sélectionnées ont été ignorées faute d'autorisation de lecture.
TimesheetWeekSummaryMissingUser = Le salarié lié à une feuille sélectionnée n'existe plus.
Expand Down
62 changes: 62 additions & 0 deletions lib/timesheetweek_pdf.lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,68 @@
dol_include_once('/timesheetweek/class/timesheetweek.class.php');

defined('TIMESHEETWEEK_PDF_SUMMARY_SUBDIR') || define('TIMESHEETWEEK_PDF_SUMMARY_SUBDIR', 'summaries');
defined('TIMESHEETWEEK_PDF_MERGED_SUBDIR') || define('TIMESHEETWEEK_PDF_MERGED_SUBDIR', 'merged');

/**
* EN: Generate the PDF of a single timesheet in the standard directory and copy it to the provided temporary folder.
* FR: Génère le PDF d'une feuille dans le répertoire standard et le copie vers le dossier temporaire fourni.
*
* @param TimesheetWeek $sheet Timesheet to render / Feuille à rendre
* @param Conf $conf Dolibarr global configuration / Configuration globale Dolibarr
* @param Translate $langs Translation handler / Gestionnaire de traductions
* @param string $modelId PDF model identifier / Identifiant du modèle PDF
* @param string $tempDir Destination temporary directory / Répertoire temporaire cible
* @return array{success:bool,path?:string,errors?:string[]}
*/
function tw_generate_timesheet_pdf_to_temp(TimesheetWeek $sheet, Conf $conf, Translate $langs, $modelId, $tempDir)
{
// EN: Ensure the temporary directory exists before writing inside it.
// FR: S'assure que le répertoire temporaire existe avant d'y écrire.
if (dol_mkdir($tempDir) < 0) {
return array('success' => false, 'errors' => array($langs->trans('ErrorCanNotCreateDir', $tempDir)));
}

$sheet->model_pdf = $modelId;

// EN: Generate the PDF using the configured or provided model.
// FR: Génère le PDF en utilisant le modèle configuré ou fourni.
$generationResult = $sheet->generateDocument($modelId, $langs);
if ($generationResult <= 0) {
$errorMessage = !empty($sheet->errors) ? implode(', ', (array) $sheet->errors) : (!empty($sheet->error) ? $sheet->error : $langs->trans('TimesheetWeekMassMergeGenerationFailed', ($sheet->ref ?: '#'.$sheet->id)));
return array('success' => false, 'errors' => array($errorMessage));
}

// EN: Locate the freshly generated PDF inside the standard output directory.
// FR: Localise le PDF fraîchement généré dans le répertoire de sortie standard.
$entityId = !empty($sheet->entity) ? (int) $sheet->entity : (int) $conf->entity;
$baseOutput = !empty($conf->timesheetweek->multidir_output[$entityId] ?? null)
? $conf->timesheetweek->multidir_output[$entityId]
: (!empty($conf->timesheetweek->dir_output) ? $conf->timesheetweek->dir_output : DOL_DATA_ROOT.'/timesheetweek');
$relativePath = $sheet->last_main_doc;
if (empty($relativePath)) {
$cleanRef = dol_sanitizeFileName($sheet->ref ?: 'timesheetweek-'.$sheet->id);
$relativePath = $sheet->element.'/'.$cleanRef.'/'.$cleanRef.'.pdf';
}

$sourcePath = rtrim($baseOutput, '/').'/'.ltrim($relativePath, '/');
if (!dol_is_file($sourcePath)) {
return array('success' => false, 'errors' => array($langs->trans('TimesheetWeekMassMergeMissingFile', ($sheet->ref ?: '#'.$sheet->id))));
}

// EN: Copy the generated PDF into the temporary directory to prepare the merge.
// FR: Copie le PDF généré dans le répertoire temporaire pour préparer la fusion.
$tempFilename = dol_sanitizeFileName(($sheet->ref ?: 'timesheetweek-'.$sheet->id).'-'.dol_print_date(dol_now(), 'dayhourlog').'.pdf');
if ($tempFilename === '') {
$tempFilename = dol_sanitizeFileName('timesheetweek-'.$sheet->id.'.pdf');
}
$tempPath = rtrim($tempDir, '/').'/'.$tempFilename;

if (dol_copy($sourcePath, $tempPath, 0, 1) <= 0) {
return array('success' => false, 'errors' => array($langs->trans('ErrorFailToCreateFile')));
}

return array('success' => true, 'path' => $tempPath);
}

/**
* EN: Normalise a value before sending it to TCPDF by decoding HTML entities and applying the output charset.
Expand Down
157 changes: 157 additions & 0 deletions timesheetweek_list.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ function tw_render_timesheet_pdf_dropdown($rowData, Conf $conf, Translate $langs
// FR: Autorise la génération d'un PDF de synthèse à tout utilisateur habilité à lire les feuilles listées.
if ($permissiontoread) {
$arrayofmassactions['generate_summary_pdf'] = img_picto('', 'pdf', 'class="pictofixedwidth"').$langs->trans('GenerateSummaryPdf');
$arrayofmassactions['builddoc_merge_pdf'] = img_picto('', 'pdf', 'class="pictofixedwidth"').$langs->trans('TimesheetWeekMassMergePdf');
}
// EN: Expose the draft-only bulk deletion with Dolibarr's confirmation flow when the operator may delete sheets.
// FR: Expose la suppression massive limitée aux brouillons avec la confirmation Dolibarr lorsque l'opérateur peut supprimer des feuilles.
Expand Down Expand Up @@ -600,6 +601,162 @@ function tw_render_timesheet_pdf_dropdown($rowData, Conf $conf, Translate $langs
}
}
}
if ($massaction === 'builddoc_merge_pdf') {
$massActionProcessed = true;
if (!$permissiontoread) {
// EN: Block the merge when the operator cannot read the selected sheets.
// FR: Bloque la fusion lorsque l'opérateur ne peut pas lire les feuilles sélectionnées.
setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors');
} elseif (empty($arrayofselected)) {
// EN: Inform the user that a selection is required before launching the merge.
// FR: Informe l'utilisateur qu'une sélection est nécessaire avant de lancer la fusion.
setEventMessages($langs->trans('TimesheetWeekErrorNoSelection'), null, 'errors');
} else {
dol_include_once('/timesheetweek/lib/timesheetweek_pdf.lib.php');
$defaultModel = getDolGlobalString('TIMESHEETWEEK_ADDON_PDF', 'standard_timesheetweek');
$tempDir = !empty($conf->timesheetweek->dir_temp) ? $conf->timesheetweek->dir_temp : (rtrim($uploaddir, '/').'/temp');
$generatedFiles = array();
$errors = array();
$warnings = array();

foreach ((array) $arrayofselected as $id) {
$id = (int) $id;
if ($id <= 0) {
continue;
}

$sheet = new TimesheetWeek($db);
if ($sheet->fetch($id) <= 0) {
$errors[] = $langs->trans('ErrorRecordNotFound').' (#'.$id.')';
continue;
}

if (!tw_can_act_on_user($sheet->fk_user, $permRead, $permReadChild, ($permReadAll || !empty($user->admin)), $user)) {
// EN: Skip the timesheet when the user cannot access the employee scope.
// FR: Ignore la feuille lorsque l'utilisateur ne peut pas accéder au périmètre du salarié.
$warnings[] = $langs->trans('TimesheetWeekMassMergeForbidden', ($sheet->ref ?: '#'.$id));
continue;
}

$tempResult = tw_generate_timesheet_pdf_to_temp($sheet, $conf, $langs, $defaultModel, $tempDir);
if (empty($tempResult['success'])) {
// EN: Propagate detailed generation issues for transparency.
// FR: Propage les problèmes de génération pour plus de transparence.
if (!empty($tempResult['errors'])) {
$errors = array_merge($errors, (array) $tempResult['errors']);
} else {
$errors[] = $langs->trans('TimesheetWeekMassMergeGenerationFailed', ($sheet->ref ?: '#'.$id));
}
continue;
}

$generatedFiles[] = $tempResult['path'];
}

if (empty($generatedFiles)) {
// EN: Stop early when no PDF could be prepared.
// FR: Arrête immédiatement lorsqu'aucun PDF n'a pu être préparé.
if (!empty($errors)) {
setEventMessages(null, array_values(array_unique($errors)), 'errors');
}
if (!empty($warnings)) {
setEventMessages(null, array_values(array_unique($warnings)), 'warnings');
}
setEventMessages($langs->trans('TimesheetWeekMassMergeNoEligible'), null, 'errors');
} else {
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';

$mergedDir = rtrim($uploaddir, '/').'/'.TIMESHEETWEEK_PDF_MERGED_SUBDIR;
$mergedFilename = dol_sanitizeFileName('timesheetweek_merged_'.dol_print_date(dol_now(), 'dayhourlog').'.pdf');
if ($mergedFilename === '') {
$mergedFilename = dol_sanitizeFileName('timesheetweek_merged.pdf');
}
$mergedFullPath = $mergedDir.'/'.$mergedFilename;
$mergeSucceeded = false;

if (dol_mkdir($mergedDir) < 0) {
setEventMessages($langs->trans('ErrorCanNotCreateDir', $mergedDir), null, 'errors');
} elseif (getDolGlobalString('USE_PDFTK_FOR_PDF_CONCAT')) {
$inputFiles = '';
foreach ($generatedFiles as $f) {
$inputFiles .= ' '.escapeshellarg($f);
}

exec('pdftk'.$inputFiles.' cat output '.escapeshellarg($mergedFullPath));
if (dol_is_file($mergedFullPath)) {
dolChmod($mergedFullPath);
$mergeSucceeded = true;
} else {
$errors[] = $langs->trans('ErrorPDFTkOutputFileNotFound');
}
} else {
$formatarray = pdf_getFormat();
$pageLargeur = $formatarray['width'];
$pageHauteur = $formatarray['height'];
$format = array($pageLargeur, $pageHauteur);

$pdf = pdf_getInstance($format);
if (class_exists('TCPDF')) {
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
}
$pdf->SetFont(pdf_getPDFFont($langs));
if (getDolGlobalString('MAIN_DISABLE_PDF_COMPRESSION')) {
$pdf->SetCompression(false);
}

$pagecount = 0;
foreach ($generatedFiles as $filePath) {
$pagecount = $pdf->setSourceFile($filePath);
for ($i = 1; $i <= $pagecount; $i++) {
$tplidx = $pdf->importPage($i);
$templatesize = $pdf->getTemplatesize($tplidx);
$pdf->AddPage($templatesize['h'] > $templatesize['w'] ? 'P' : 'L');
$pdf->useTemplate($tplidx);
}
}

if ($pagecount > 0) {
$pdf->Output($mergedFullPath, 'F');
dolChmod($mergedFullPath);
$mergeSucceeded = true;
} else {
$errors[] = $langs->trans('NoPDFAvailableForDocGenAmongChecked');
}
}

foreach ($generatedFiles as $tempFilePath) {
if (dol_is_file($tempFilePath)) {
dol_delete_file($tempFilePath, 0, 0, 0, null, false);
}
}

if (!$mergeSucceeded) {
// EN: Inform the operator that merging failed.
// FR: Informe l'opérateur que la fusion a échoué.
if (!empty($warnings)) {
setEventMessages(null, array_values(array_unique($warnings)), 'warnings');
}
if (!empty($errors)) {
setEventMessages(null, array_values(array_unique($errors)), 'warnings');
}
setEventMessages($langs->trans('TimesheetWeekMassMergeError'), null, 'errors');
} else {
$relativeMerged = TIMESHEETWEEK_PDF_MERGED_SUBDIR.'/'.$mergedFilename;
if (!empty($warnings)) {
setEventMessages(null, array_values(array_unique($warnings)), 'warnings');
}
if (!empty($errors)) {
setEventMessages(null, array_values(array_unique($errors)), 'warnings');
}
setEventMessages($langs->trans('TimesheetWeekMassMergeSuccess'), null, 'mesgs');
$downloadUrl = DOL_URL_ROOT.'/document.php?modulepart=timesheetweek&file='.urlencode($relativeMerged).'&entity='.$conf->entity.'&permission=read';
header('Location: '.$downloadUrl);
exit;
}
}
}
}
if ($massaction === 'delete') {
$massActionProcessed = true;
if (!$permissiontodelete) {
Expand Down