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['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) {