From 3635cd132e1af8f7feacf903e2f48e66c3b97fa3 Mon Sep 17 00:00:00 2001 From: Simon Schaufelberger Date: Tue, 19 Nov 2019 21:31:33 +0100 Subject: [PATCH] Grouped entries --- extensions/ki_export/floaters.php | 4 + extensions/ki_export/private_func.php | 14 +++- extensions/ki_export/processor.php | 30 ++++--- .../templates/scripts/floaters/export_CSV.php | 6 +- .../templates/scripts/floaters/export_PDF.php | 4 + .../templates/scripts/floaters/export_XLS.php | 4 + .../templates/scripts/floaters/print.php | 4 + language/de.php | 1 + language/en.php | 1 + libraries/Kimai/Database/Mysql.php | 78 ++++++++++++++----- 10 files changed, 114 insertions(+), 32 deletions(-) diff --git a/extensions/ki_export/floaters.php b/extensions/ki_export/floaters.php index c90b30256..630b7ff5b 100644 --- a/extensions/ki_export/floaters.php +++ b/extensions/ki_export/floaters.php @@ -33,6 +33,7 @@ 'download_pdf' => 1, 'customer_new_page' => 0, 'reverse_order' => 0, + 'grouped_entries' => 0, 'pdf_format' => 'export_pdf', 'time_type' => 'dec_time' ]; @@ -45,6 +46,7 @@ case 'XLS': $defaults = [ 'reverse_order' => 0, + 'grouped_entries' => 0, ]; $prefs = $database->user_get_preferences_by_prefix('ki_export.xls.'); $view->assign('prefs', array_merge($defaults, $prefs)); @@ -57,6 +59,7 @@ 'column_delimiter' => ',', 'quote_char' => '"', 'reverse_order' => 0, + 'grouped_entries' => 0, ]; $prefs = $database->user_get_preferences_by_prefix('ki_export.csv.'); $view->assign('prefs', array_merge($defaults, $prefs)); @@ -68,6 +71,7 @@ $defaults = [ 'print_summary' => 1, 'reverse_order' => 0, + 'grouped_entries' => 0, ]; $prefs = $database->user_get_preferences_by_prefix('ki_export.print.'); $view->assign('prefs', array_merge($defaults, $prefs)); diff --git a/extensions/ki_export/private_func.php b/extensions/ki_export/private_func.php index 3591fb8c2..9a0a3dee5 100644 --- a/extensions/ki_export/private_func.php +++ b/extensions/ki_export/private_func.php @@ -63,6 +63,7 @@ * @param int $filter_type (-1 show time and expenses, 0: only show time entries, 1: only show expenses) * @param bool $limitCommentSize should comments be cut off, when they are too long * @param int $filter_refundable + * @param bool $groupedEntries * @return array with time recordings and expenses chronologically sorted */ function export_get_data( @@ -78,7 +79,8 @@ function export_get_data( $filter_cleared = -1, $filter_type = -1, $limitCommentSize = true, - $filter_refundable = -1 + $filter_refundable = -1, + $groupedEntries = false ) { global $expense_ext_available; $database = Kimai_Registry::getDatabase(); @@ -94,7 +96,11 @@ function export_get_data( $activities, $limit, $reverse_order, - $filter_cleared + $filter_cleared, + 0, + 0, + false, + $groupedEntries ); } @@ -160,7 +166,9 @@ function export_get_data( if ($timeSheetEntries[$timeSheetEntries_index]['end'] != 0) { // active recordings will be omitted $arr['type'] = 'timeSheet'; - $arr['id'] = $timeSheetEntries[$timeSheetEntries_index]['timeEntryID']; + if (isset($timeSheetEntries[$timeSheetEntries_index]['timeEntryID'])) { + $arr['id'] = $timeSheetEntries[$timeSheetEntries_index]['timeEntryID']; + } $arr['time_in'] = $timeSheetEntries[$timeSheetEntries_index]['start']; $arr['time_out'] = $timeSheetEntries[$timeSheetEntries_index]['end']; $arr['duration'] = $timeSheetEntries[$timeSheetEntries_index]['duration']; diff --git a/extensions/ki_export/processor.php b/extensions/ki_export/processor.php index 4afc34758..614b8732e 100644 --- a/extensions/ki_export/processor.php +++ b/extensions/ki_export/processor.php @@ -55,6 +55,7 @@ $default_location = strip_tags($_REQUEST['default_location']); $reverse_order = isset($_REQUEST['reverse_order']); + $grouped_entries = isset($_REQUEST['grouped_entries']); $filter_cleared = $_REQUEST['filter_cleared']; $filter_refundable = $_REQUEST['filter_refundable']; @@ -197,7 +198,8 @@ case 'export_html': $database->user_set_preferences([ 'print_summary' => isset($_REQUEST['print_summary']) ? 1 : 0, - 'reverse_order' => isset($_REQUEST['reverse_order']) ? 1 : 0 + 'reverse_order' => isset($_REQUEST['reverse_order']) ? 1 : 0, + 'grouped_entries' => isset($_REQUEST['grouped_entries']) ? 1 : 0 ], 'ki_export.print.'); $exportData = export_get_data( @@ -213,7 +215,8 @@ $filter_cleared, $filter_type, false, - $filter_refundable + $filter_refundable, + $grouped_entries ); $timeSum = 0; $wageSum = 0; @@ -298,7 +301,8 @@ case 'export_xls': $database->user_set_preferences([ - 'reverse_order' => isset($_REQUEST['reverse_order']) ? 1 : 0 + 'reverse_order' => isset($_REQUEST['reverse_order']) ? 1 : 0, + 'grouped_entries' => isset($_REQUEST['grouped_entries']) ? 1 : 0 ], 'ki_export.xls.'); $exportData = export_get_data( @@ -314,7 +318,8 @@ $filter_cleared, $filter_type, false, - $filter_refundable + $filter_refundable, + $grouped_entries ); $view->assign('exportData', count($exportData) > 0 ? $exportData : 0); @@ -332,7 +337,8 @@ $database->user_set_preferences([ 'column_delimiter' => $_REQUEST['column_delimiter'], 'quote_char' => $_REQUEST['quote_char'], - 'reverse_order' => isset($_REQUEST['reverse_order']) ? 1 : 0 + 'reverse_order' => isset($_REQUEST['reverse_order']) ? 1 : 0, + 'grouped_entries' => isset($_REQUEST['grouped_entries']) ? 1 : 0 ], 'ki_export.csv.'); $exportData = export_get_data( @@ -348,13 +354,15 @@ $filter_cleared, $filter_type, false, - $filter_refundable + $filter_refundable, + $grouped_entries ); $column_delimiter = $_REQUEST['column_delimiter']; $quote_char = $_REQUEST['quote_char']; + header('Content-Encoding: UTF-8'); header('Content-Disposition:attachment;filename=export.csv'); - header('Content-Type: text/csv'); + header('Content-Type: text/csv; charset=UTF-8'); $row = []; @@ -503,6 +511,7 @@ 'download_pdf' => isset($_REQUEST['download_pdf']) ? 1 : 0, 'customer_new_page' => isset($_REQUEST['customer_new_page']) ? 1 : 0, 'reverse_order' => isset($_REQUEST['reverse_order']) ? 1 : 0, + 'grouped_entries' => isset($_REQUEST['grouped_entries']) ? 1 : 0, 'time_type' => 'dec_time', 'pdf_format' => 'export_pdf' ], 'ki_export.pdf.'); @@ -520,7 +529,8 @@ $filter_cleared, $filter_type, false, - $filter_refundable + $filter_refundable, + $grouped_entries ); $orderedExportData = []; @@ -556,6 +566,7 @@ 'download_pdf' => isset($_REQUEST['download_pdf']) ? 1 : 0, 'customer_new_page' => isset($_REQUEST['customer_new_page']) ? 1 : 0, 'reverse_order' => isset($_REQUEST['reverse_order']) ? 1 : 0, + 'grouped_entries' => isset($_REQUEST['grouped_entries']) ? 1 : 0, 'pdf_format' => 'export_pdf2' ], 'ki_export.pdf.'); @@ -572,7 +583,8 @@ $filter_cleared, $filter_type, false, - $filter_refundable + $filter_refundable, + $grouped_entries ); // sort data into new array, where first dimension is customer and second dimension is project diff --git a/extensions/ki_export/templates/scripts/floaters/export_CSV.php b/extensions/ki_export/templates/scripts/floaters/export_CSV.php index 7ec2f75b0..0d1da31cd 100644 --- a/extensions/ki_export/templates/scripts/floaters/export_CSV.php +++ b/extensions/ki_export/templates/scripts/floaters/export_CSV.php @@ -33,7 +33,11 @@
  • - + prefs['reverse_order']): ?> checked="checked" /> +
  • +
  • + + prefs['grouped_entries']): ?> checked="checked" />
  • translate('export_extension:dl_hint') ?> diff --git a/extensions/ki_export/templates/scripts/floaters/export_PDF.php b/extensions/ki_export/templates/scripts/floaters/export_PDF.php index b8397a46b..3dafda8c7 100644 --- a/extensions/ki_export/templates/scripts/floaters/export_PDF.php +++ b/extensions/ki_export/templates/scripts/floaters/export_PDF.php @@ -46,6 +46,10 @@ prefs['reverse_order']): ?> checked="checked" />
  • +
  • + + prefs['grouped_entries']): ?> checked="checked" /> +
  • prefs['reverse_order']): ?> checked="checked" />
  • +
  • + + prefs['grouped_entries']): ?> checked="checked" /> +
  • translate('export_extension:dl_hint') ?>
  • diff --git a/extensions/ki_export/templates/scripts/floaters/print.php b/extensions/ki_export/templates/scripts/floaters/print.php index 8856afada..9d34b47e1 100644 --- a/extensions/ki_export/templates/scripts/floaters/print.php +++ b/extensions/ki_export/templates/scripts/floaters/print.php @@ -34,6 +34,10 @@ prefs['reverse_order']): ?> checked="checked" /> +
  • + + prefs['grouped_entries']): ?> checked="checked" /> +
  • diff --git a/language/de.php b/language/de.php index 583597eae..7fc2794bb 100644 --- a/language/de.php +++ b/language/de.php @@ -457,6 +457,7 @@ 'times' => 'Zeiten', 'expenses' => 'Auslagen', 'reverse_order' => 'Ältere Einträge zuerst', + 'grouped_entries' => 'Gruppierte Einträge', 'time_period' => 'Zeitraum', 'duration_unit' => 'Std.', 'cleared' => 'Abgerechnet', diff --git a/language/en.php b/language/en.php index d117800d0..c50a2f9c6 100644 --- a/language/en.php +++ b/language/en.php @@ -459,6 +459,7 @@ 'times' => 'times', 'expenses' => 'expenses', 'reverse_order' => 'Older entries first', + 'grouped_entries' => 'Grouped entries', 'time_period' => 'Time period', 'duration_unit' => 'h', 'time_type' => 'Time format', diff --git a/libraries/Kimai/Database/Mysql.php b/libraries/Kimai/Database/Mysql.php index 4a60fa789..08073d2dd 100644 --- a/libraries/Kimai/Database/Mysql.php +++ b/libraries/Kimai/Database/Mysql.php @@ -2675,6 +2675,7 @@ public function timeSheet_whereClausesFromFilters($users, $customers, $projects, * @param int $startRows * @param int $limitRows * @param bool $countOnly + * @param bool $groupedEntries * @return array */ public function get_timeSheet( @@ -2689,7 +2690,8 @@ public function get_timeSheet( $filterCleared = null, $startRows = 0, $limitRows = 0, - $countOnly = false + $countOnly = false, + $groupedEntries = false ) { // -1 for disabled, 0 for only not cleared entries if (!is_numeric($filterCleared)) { @@ -2735,27 +2737,62 @@ public function get_timeSheet( $limit = ''; } - $select = 'SELECT timeSheet.*, - status.status, - customer.name AS customerName, customer.customerID as customerID, - activity.name AS activityName, - project.name AS projectName, project.comment AS projectComment, - user.name AS userName, user.alias AS userAlias '; - - if ($countOnly) { - $select = 'SELECT COUNT(*) AS total'; - $limit = ''; - } + if ($groupedEntries) { + $query = "SELECT + DATE_FORMAT(FROM_UNIXTIME(`start`), '%Y-%m-%d') AS `aggrDate`, + MIN(`start`) AS `start`, + MAX(`end`) AS `end`, + SUM(`duration`) AS `duration`, + GROUP_CONCAT(DISTINCT `userID`) AS `userID`, + `projectID`, + `activityID`, + GROUP_CONCAT(`description` SEPARATOR '\\n') AS `description`, + GROUP_CONCAT(`timeSheet`.`comment` SEPARATOR ', ') AS `comment`, + `commentType`, + `cleared`, + GROUP_CONCAT(`location` SEPARATOR ', ') AS `location`, + GROUP_CONCAT(`trackingNumber` SEPARATOR ', ') AS `trackingNumber`, + `rate`, + `fixedRate`, + `timeSheet`.`budget`, + MIN(`timeSheet`.`approved`) as `approved`, + `statusID`, + MIN(`billable`) as `billable`, + `customer`.`name` AS `customerName`, `customer`.`customerID` as `customerID`, + `activity`.`name` AS `activityName`, + `project`.`name` AS `projectName`, `project`.`comment` AS `projectComment`, + `user`.`name` AS `userName`, `user`.`alias` AS `userAlias` + FROM `${p}timeSheet` AS `timeSheet` + JOIN `${p}projects` AS `project` USING (`projectID`) + JOIN `${p}customers` AS `customer` USING (`customerID`) + JOIN `${p}activities` AS `activity` USING (`activityID`) + JOIN `${p}users` AS `user` USING (`userID`) " + . (count($whereClauses) > 0 ? ' WHERE ' : ' ') . implode(' AND ', $whereClauses) . + ' GROUP BY `aggrDate`, `projectID`, `activityID`, `rate`, `fixedRate`' . + ' ORDER BY `start` ' . ($reverse_order ? 'ASC ' : 'DESC ') . $limit . ';'; + } else { + $select = 'SELECT timeSheet.*, + status.status, + customer.name AS customerName, customer.customerID as customerID, + activity.name AS activityName, + project.name AS projectName, project.comment AS projectComment, + user.name AS userName, user.alias AS userAlias '; + + if ($countOnly) { + $select = 'SELECT COUNT(*) AS total'; + $limit = ''; + } - $query = "$select + $query = "$select FROM ${p}timeSheet AS timeSheet JOIN ${p}projects AS project USING (projectID) JOIN ${p}customers AS customer USING (customerID) - JOIN ${p}users AS user USING(userID) - JOIN ${p}statuses AS status USING(statusID) - JOIN ${p}activities AS activity USING(activityID) " - . (count($whereClauses) > 0 ? ' WHERE ' : ' ') . implode(' AND ', $whereClauses) . - ' ORDER BY start ' . ($reverse_order ? 'ASC ' : 'DESC ') . $limit . ';'; + JOIN ${p}users AS user USING (userID) + JOIN ${p}statuses AS status USING (statusID) + JOIN ${p}activities AS activity USING (activityID) " + . (count($whereClauses) > 0 ? ' WHERE ' : ' ') . implode(' AND ', $whereClauses) . + ' ORDER BY start ' . ($reverse_order ? 'ASC ' : 'DESC ') . $limit . ';'; + } $result = $this->conn->Query($query); @@ -2775,7 +2812,10 @@ public function get_timeSheet( $this->conn->MoveFirst(); while (!$this->conn->EndOfSeek()) { $row = $this->conn->Row(); - $arr[$i]['timeEntryID'] = $row->timeEntryID; + + if (!$groupedEntries) { + $arr[$i]['timeEntryID'] = $row->timeEntryID; + } // Start time should not be less than the selected start time. This would confuse the user. if ($start && $row->start <= $start) {