Refactoring ttReportHelper for timesheets.
[timetracker.git] / WEB-INF / lib / ttReportHelper.class.php
1 <?php
2 // +----------------------------------------------------------------------+
3 // | Anuko Time Tracker
4 // +----------------------------------------------------------------------+
5 // | Copyright (c) Anuko International Ltd. (https://www.anuko.com)
6 // +----------------------------------------------------------------------+
7 // | LIBERAL FREEWARE LICENSE: This source code document may be used
8 // | by anyone for any purpose, and freely redistributed alone or in
9 // | combination with other software, provided that the license is obeyed.
10 // |
11 // | There are only two ways to violate the license:
12 // |
13 // | 1. To redistribute this code in source form, with the copyright
14 // |    notice or license removed or altered. (Distributing in compiled
15 // |    forms without embedded copyright notices is permitted).
16 // |
17 // | 2. To redistribute modified versions of this code in *any* form
18 // |    that bears insufficient indications that the modifications are
19 // |    not the work of the original author(s).
20 // |
21 // | This license applies to this document only, not any other software
22 // | that it may be combined with.
23 // |
24 // +----------------------------------------------------------------------+
25 // | Contributors:
26 // | https://www.anuko.com/time_tracker/credits.htm
27 // +----------------------------------------------------------------------+
28
29 import('ttClientHelper');
30 import('DateAndTime');
31 import('Period');
32 import('ttTimeHelper');
33
34 require_once(dirname(__FILE__).'/../../plugins/CustomFields.class.php');
35
36 // Class ttReportHelper is used for help with reports.
37 class ttReportHelper {
38
39   // getWhere prepares a WHERE clause for a report query.
40   static function getWhere($options) {
41     global $user;
42
43     $group_id = $user->getGroup();
44     $org_id = $user->org_id;
45
46     // Prepare dropdown parts.
47     $dropdown_parts = '';
48     if ($options['client_id'])
49       $dropdown_parts .= ' and l.client_id = '.$options['client_id'];
50     elseif ($user->isClient() && $user->client_id)
51       $dropdown_parts .= ' and l.client_id = '.$user->client_id;
52     if ($options['cf_1_option_id']) $dropdown_parts .= ' and l.id in(select log_id from tt_custom_field_log where status = 1 and option_id = '.$options['cf_1_option_id'].')';
53     if ($options['project_id']) $dropdown_parts .= ' and l.project_id = '.$options['project_id'];
54     if ($options['task_id']) $dropdown_parts .= ' and l.task_id = '.$options['task_id'];
55     if ($options['billable']=='1') $dropdown_parts .= ' and l.billable = 1';
56     if ($options['billable']=='2') $dropdown_parts .= ' and l.billable = 0';
57     if ($options['invoice']=='1') $dropdown_parts .= ' and l.invoice_id is not NULL';
58     if ($options['invoice']=='2') $dropdown_parts .= ' and l.invoice_id is NULL';
59     if ($options['paid_status']=='1') $dropdown_parts .= ' and l.paid = 1';
60     if ($options['paid_status']=='2') $dropdown_parts .= ' and l.paid = 0';
61
62     // Prepare sql query part for user list.
63     $userlist = $options['users'] ? $options['users'] : '-1';
64     if ($user->can('view_reports') || $user->can('view_all_reports') || $user->isClient())
65       $user_list_part = " and l.user_id in ($userlist)";
66     else
67       $user_list_part = " and l.user_id = ".$user->getUser();
68     $user_list_part .= " and l.group_id = $group_id and l.org_id = $org_id";
69
70     // Prepare sql query part for where.
71     $dateFormat = $user->getDateFormat();
72     if ($options['period'])
73       $period = new Period($options['period'], new DateAndTime($dateFormat));
74     else {
75       $period = new Period();
76       $period->setPeriod(
77         new DateAndTime($dateFormat, $options['period_start']),
78         new DateAndTime($dateFormat, $options['period_end']));
79     }
80     $where = " where l.status = 1 and l.date >= '".$period->getStartDate(DB_DATEFORMAT)."' and l.date <= '".$period->getEndDate(DB_DATEFORMAT)."'".
81       " $user_list_part $dropdown_parts";
82     return $where;
83   }
84
85   // getExpenseWhere prepares WHERE clause for expenses query in a report.
86   static function getExpenseWhere($options) {
87     global $user;
88
89     $group_id = $user->getGroup();
90     $org_id = $user->org_id;
91
92     // Prepare dropdown parts.
93     $dropdown_parts = '';
94     if ($options['client_id'])
95       $dropdown_parts .= ' and ei.client_id = '.$options['client_id'];
96     elseif ($user->isClient() && $user->client_id)
97       $dropdown_parts .= ' and ei.client_id = '.$user->client_id;
98     if ($options['project_id']) $dropdown_parts .= ' and ei.project_id = '.$options['project_id'];
99     if ($options['invoice']=='1') $dropdown_parts .= ' and ei.invoice_id is not NULL';
100     if ($options['invoice']=='2') $dropdown_parts .= ' and ei.invoice_id is NULL';
101     if ($options['paid_status']=='1') $dropdown_parts .= ' and ei.paid = 1';
102     if ($options['paid_status']=='2') $dropdown_parts .= ' and ei.paid = 0';
103
104     // Prepare sql query part for user list.
105     $userlist = $options['users'] ? $options['users'] : '-1';
106     if ($user->can('view_reports') || $user->can('view_all_reports') || $user->isClient())
107       $user_list_part = " and ei.user_id in ($userlist)";
108     else
109       $user_list_part = " and ei.user_id = ".$user->getUser();
110     $user_list_part .= " and ei.group_id = $group_id and ei.org_id = $org_id";
111
112     // Prepare sql query part for where.
113     $dateFormat = $user->getDateFormat();
114     if ($options['period'])
115       $period = new Period($options['period'], new DateAndTime($dateFormat));
116     else {
117       $period = new Period();
118       $period->setPeriod(
119         new DateAndTime($dateFormat, $options['period_start']),
120         new DateAndTime($dateFormat, $options['period_end']));
121     }
122     $where = " where ei.status = 1 and ei.date >= '".$period->getStartDate(DB_DATEFORMAT)."' and ei.date <= '".$period->getEndDate(DB_DATEFORMAT)."'".
123       " $user_list_part $dropdown_parts";
124     return $where;
125   }
126
127   // getItems retrieves all items associated with a report.
128   // It combines tt_log and tt_expense_items in one array for presentation in one table using mysql union all.
129   // Expense items use the "note" field for item name.
130   static function getItems($options) {
131     global $user;
132     $mdb2 = getConnection();
133
134     // Determine these once as they are used in multiple places in this function.
135     $canViewReports = $user->can('view_reports') || $user->can('view_all_reports');
136     $isClient = $user->isClient();
137
138     $grouping = ttReportHelper::grouping($options);
139     if ($grouping) {
140       $grouping_by_date = ttReportHelper::groupingBy('date', $options);
141       $grouping_by_client = ttReportHelper::groupingBy('client', $options);
142       $grouping_by_project = ttReportHelper::groupingBy('project', $options);
143       $grouping_by_task = ttReportHelper::groupingBy('task', $options);
144       $grouping_by_user = ttReportHelper::groupingBy('user', $options);
145       $grouping_by_cf_1 = ttReportHelper::groupingBy('cf_1', $options);
146     }
147     $convertTo12Hour = ('%I:%M %p' == $user->getTimeFormat()) && ($options['show_start'] || $options['show_end']);
148     $trackingMode = $user->getTrackingMode();
149     $decimalMark = $user->getDecimalMark();
150
151     // Prepare a query for time items in tt_log table.
152     $fields = array(); // An array of fields for database query.
153     array_push($fields, 'l.id');
154     array_push($fields, 'l.user_id');
155     array_push($fields, '1 as type'); // Type 1 is for tt_log entries.
156     array_push($fields, 'l.date');
157     if($canViewReports || $isClient)
158       array_push($fields, 'u.name as user');
159     // Add client name if it is selected.
160     if ($options['show_client'] || $grouping_by_client)
161       array_push($fields, 'c.name as client');
162     // Add project name if it is selected.
163     if ($options['show_project'] || $grouping_by_project)
164       array_push($fields, 'p.name as project');
165     // Add task name if it is selected.
166     if ($options['show_task'] || $grouping_by_task)
167       array_push($fields, 't.name as task');
168     // Add custom field.
169     $include_cf_1 = $options['show_custom_field_1'] || $grouping_by_cf_1;
170     if ($include_cf_1) {
171       $custom_fields = new CustomFields();
172       $cf_1_type = $custom_fields->fields[0]['type'];
173       if ($cf_1_type == CustomFields::TYPE_TEXT) {
174         array_push($fields, 'cfl.value as cf_1');
175       } elseif ($cf_1_type == CustomFields::TYPE_DROPDOWN) {
176         array_push($fields, 'cfo.value as cf_1');
177       }
178     }
179     // Add start time.
180     if ($options['show_start']) {
181       array_push($fields, "l.start as unformatted_start");
182       array_push($fields, "TIME_FORMAT(l.start, '%k:%i') as start");
183     }
184     // Add finish time.
185     if ($options['show_end'])
186       array_push($fields, "TIME_FORMAT(sec_to_time(time_to_sec(l.start) + time_to_sec(l.duration)), '%k:%i') as finish");
187     // Add duration.
188     if ($options['show_duration'])
189       array_push($fields, "TIME_FORMAT(l.duration, '%k:%i') as duration");
190     // Add work units.
191     if ($options['show_work_units']) {
192       if ($user->getConfigOption('unit_totals_only'))
193         array_push($fields, "null as units");
194       else {
195         $firstUnitThreshold = $user->getConfigInt('1st_unit_threshold', 0);
196         $minutesInUnit = $user->getConfigInt('minutes_in_unit', 15);
197         array_push($fields, "if(l.billable = 0 or time_to_sec(l.duration)/60 < $firstUnitThreshold, 0, ceil(time_to_sec(l.duration)/60/$minutesInUnit)) as units");
198       }
199     }
200     // Add note.
201     if ($options['show_note'])
202       array_push($fields, 'l.comment as note');
203     // Handle cost.
204     $includeCost = $options['show_cost'];
205     if ($includeCost) {
206       if (MODE_TIME == $trackingMode)
207         array_push($fields, "cast(l.billable * coalesce(u.rate, 0) * time_to_sec(l.duration)/3600 as decimal(10,2)) as cost");   // Use default user rate.
208       else
209         array_push($fields, "cast(l.billable * coalesce(upb.rate, 0) * time_to_sec(l.duration)/3600 as decimal(10,2)) as cost"); // Use project rate for user.
210       array_push($fields, "null as expense"); 
211     }
212     // Add paid status.
213     if ($canViewReports && $options['show_paid'])
214       array_push($fields, 'l.paid');
215     // Add IP address.
216     if ($canViewReports && $options['show_ip']) {
217       array_push($fields, 'l.created');
218       array_push($fields, 'l.created_ip');
219       array_push($fields, 'l.modified');
220       array_push($fields, 'l.modified_ip');
221     }
222     // Add invoice name if it is selected.
223     if (($canViewReports || $isClient) && $options['show_invoice'])
224       array_push($fields, 'i.name as invoice');
225
226     // Prepare sql query part for left joins.
227     $left_joins = null;
228     if ($options['show_client'] || $grouping_by_client)
229       $left_joins .= " left join tt_clients c on (c.id = l.client_id)";
230     if (($canViewReports || $isClient) && $options['show_invoice'])
231       $left_joins .= " left join tt_invoices i on (i.id = l.invoice_id and i.status = 1)";
232     if ($canViewReports || $isClient || $user->isPluginEnabled('ex'))
233        $left_joins .= " left join tt_users u on (u.id = l.user_id)";
234     if ($options['show_project'] || $grouping_by_project)
235       $left_joins .= " left join tt_projects p on (p.id = l.project_id)";
236     if ($options['show_task'] || $grouping_by_task)
237       $left_joins .= " left join tt_tasks t on (t.id = l.task_id)";
238     if ($include_cf_1) {
239       if ($cf_1_type == CustomFields::TYPE_TEXT)
240         $left_joins .= " left join tt_custom_field_log cfl on (l.id = cfl.log_id and cfl.status = 1)";
241       elseif ($cf_1_type == CustomFields::TYPE_DROPDOWN) {
242         $left_joins .=  " left join tt_custom_field_log cfl on (l.id = cfl.log_id and cfl.status = 1)".
243           " left join tt_custom_field_options cfo on (cfl.option_id = cfo.id)";
244       }
245     }
246     if ($includeCost && MODE_TIME != $trackingMode)
247       $left_joins .= " left join tt_user_project_binds upb on (l.user_id = upb.user_id and l.project_id = upb.project_id)";
248
249     $where = ttReportHelper::getWhere($options);
250
251     // Construct sql query for tt_log items.
252     $sql = "select ".join(', ', $fields)." from tt_log l $left_joins $where";
253     // If we don't have expense items (such as when the Expenses plugin is disabled), the above is all sql we need,
254     // with an exception of sorting part, that is added in the end.
255
256     // However, when we have expenses, we need to do a union with a separate query for expense items from tt_expense_items table.
257     if ($options['show_cost'] && $user->isPluginEnabled('ex')) { // if ex(penses) plugin is enabled
258
259       $fields = array(); // An array of fields for database query.
260       array_push($fields, 'ei.id');
261       array_push($fields, 'ei.user_id');
262       array_push($fields, '2 as type'); // Type 2 is for tt_expense_items entries.
263       array_push($fields, 'ei.date');
264       if($canViewReports || $isClient)
265         array_push($fields, 'u.name as user');
266       // Add client name if it is selected.
267       if ($options['show_client'] || $grouping_by_client)
268         array_push($fields, 'c.name as client');
269       // Add project name if it is selected.
270       if ($options['show_project'] || $grouping_by_project)
271         array_push($fields, 'p.name as project');
272       if ($options['show_task'] || $grouping_by_task)
273         array_push($fields, 'null'); // null for task name. We need to match column count for union.
274       if ($options['show_custom_field_1'] || $grouping_by_cf_1)
275         array_push($fields, 'null'); // null for cf_1.
276       if ($options['show_start']) {
277         array_push($fields, 'null'); // null for unformatted_start.
278         array_push($fields, 'null'); // null for start.
279       }
280       if ($options['show_end'])
281         array_push($fields, 'null'); // null for finish.
282       if ($options['show_duration'])
283         array_push($fields, 'null'); // null for duration.
284       if ($options['show_work_units'])
285         array_push($fields, 'null as units'); // null for work units.
286       // Use the note field to print item name.
287       if ($options['show_note'])
288         array_push($fields, 'ei.name as note');
289       array_push($fields, 'ei.cost as cost');
290       array_push($fields, 'ei.cost as expense');
291       // Add paid status.
292       if ($canViewReports && $options['show_paid'])
293         array_push($fields, 'ei.paid');
294       // Add IP address.
295       if ($canViewReports && $options['show_ip']) {
296         array_push($fields, 'ei.created');
297         array_push($fields, 'ei.created_ip');
298         array_push($fields, 'ei.modified');
299         array_push($fields, 'ei.modified_ip');
300       }
301       // Add invoice name if it is selected.
302       if (($canViewReports || $isClient) && $options['show_invoice'])
303         array_push($fields, 'i.name as invoice');
304
305       // Prepare sql query part for left joins.
306       $left_joins = null;
307       if ($canViewReports || $isClient)
308         $left_joins .= " left join tt_users u on (u.id = ei.user_id)";
309       if ($options['show_client'] || $grouping_by_client)
310         $left_joins .= " left join tt_clients c on (c.id = ei.client_id)";
311       if ($options['show_project'] || $grouping_by_project)
312         $left_joins .= " left join tt_projects p on (p.id = ei.project_id)";
313       if (($canViewReports || $isClient) && $options['show_invoice'])
314         $left_joins .= " left join tt_invoices i on (i.id = ei.invoice_id and i.status = 1)";
315
316       $where = ttReportHelper::getExpenseWhere($options);
317
318       // Construct sql query for expense items.
319       $sql_for_expense_items = "select ".join(', ', $fields)." from tt_expense_items ei $left_joins $where";
320
321       // Construct a union.
322       $sql = "($sql) union all ($sql_for_expense_items)";
323     }
324
325     // Determine sort part.
326     $sort_part = ' order by ';
327     if ($grouping) {
328       $sort_part2 .= ($options['group_by1'] != null && $options['group_by1'] != 'no_grouping') ? ', '.$options['group_by1'] : '';
329       $sort_part2 .= ($options['group_by2'] != null && $options['group_by2'] != 'no_grouping') ? ', '.$options['group_by2'] : '';
330       $sort_part2 .= ($options['group_by3'] != null && $options['group_by3'] != 'no_grouping') ? ', '.$options['group_by3'] : '';
331       if (!$grouping_by_date) $sort_part2 .= ', date';
332       $sort_part .= ltrim($sort_part2, ', '); // Remove leading comma and space.
333     } else {
334       $sort_part .= 'date';
335     }
336     if (($canViewReports || $isClient) && $options['users'] && !$grouping_by_user)
337       $sort_part .= ', user, type';
338     if ($options['show_start'])
339       $sort_part .= ', unformatted_start';
340     $sort_part .= ', id';
341
342     $sql .= $sort_part;
343     // By now we are ready with sql.
344
345     // Obtain items for report.
346     $res = $mdb2->query($sql);
347     if (is_a($res, 'PEAR_Error')) die($res->getMessage());
348
349     while ($val = $res->fetchRow()) {
350       if ($convertTo12Hour) {
351         if($val['start'] != '')
352           $val['start'] = ttTimeHelper::to12HourFormat($val['start']);
353         if($val['finish'] != '')
354           $val['finish'] = ttTimeHelper::to12HourFormat($val['finish']);
355       }
356       if (isset($val['cost'])) {
357         if ('.' != $decimalMark)
358           $val['cost'] = str_replace('.', $decimalMark, $val['cost']);
359       }
360       if (isset($val['expense'])) {
361         if ('.' != $decimalMark)
362           $val['expense'] = str_replace('.', $decimalMark, $val['expense']);
363       }
364
365       if ($grouping) $val['grouped_by'] = ttReportHelper::makeGroupByKey($options, $val);
366       $val['date'] = ttDateToUserFormat($val['date']);
367
368       $report_items[] = $val;
369     }
370
371     return $report_items;
372   }
373
374   // putInSession stores tt_log and tt_expense_items ids from a report in user session
375   // as 2 comma-separated lists.
376   static function putInSession($report_items) {
377     unset($_SESSION['report_item_ids']);
378     unset($_SESSION['report_item_expense_ids']);
379
380     // Iterate through records and build 2 comma-separated lists.
381     foreach($report_items as $item) {
382       if ($item['type'] == 1)
383         $report_item_ids .= ','.$item['id'];
384       else if ($item['type'] == 2)
385          $report_item_expense_ids .= ','.$item['id'];
386     }
387     $report_item_ids = trim($report_item_ids, ',');
388     $report_item_expense_ids = trim($report_item_expense_ids, ',');
389
390     // The lists are reqdy. Put them in session.
391     if ($report_item_ids) $_SESSION['report_item_ids'] = $report_item_ids;
392     if ($report_item_expense_ids) $_SESSION['report_item_expense_ids'] = $report_item_expense_ids;
393   }
394
395   // getFromSession obtains tt_log and tt_expense_items ids stored in user session.
396   static function getFromSession() {
397     $items = array();
398     $report_item_ids = $_SESSION['report_item_ids'];
399     if ($report_item_ids)
400       $items['report_item_ids'] = explode(',', $report_item_ids);
401     $report_item_expense_ids = $_SESSION['report_item_expense_ids'];
402     if ($report_item_expense_ids)
403       $items['report_item_expense_ids'] = explode(',', $report_item_expense_ids);
404     return $items;
405   }
406
407   // getSubtotals calculates report items subtotals when a report is grouped by.
408   // Without expenses, it's a simple select with group by.
409   // With expenses, it becomes a select with group by from a combined set of records obtained with "union all".
410   static function getSubtotals($options) {
411     global $user;
412     $mdb2 = getConnection();
413
414     $concat_part = ttReportHelper::makeConcatPart($options);
415     $work_unit_part = ttReportHelper::makeWorkUnitPart($options);
416     $join_part = ttReportHelper::makeJoinPart($options);
417     $cost_part = ttReportHelper::makeCostPart($options);
418     $where = ttReportHelper::getWhere($options);
419     $group_by_part = ttReportHelper::makeGroupByPart($options);
420
421     $parts = "$concat_part, sum(time_to_sec(l.duration)) as time, null as expenses".$work_unit_part.$cost_part;
422     $sql = "select $parts from tt_log l $join_part $where $group_by_part";
423     // By now we have sql for time items.
424
425     // However, when we have expenses, we need to do a union with a separate query for expense items from tt_expense_items table.
426     if ($options['show_cost'] && $user->isPluginEnabled('ex')) { // if ex(penses) plugin is enabled
427
428       $concat_part = ttReportHelper::makeConcatExpensesPart($options);
429       $join_part = ttReportHelper::makeJoinExpensesPart($options);
430       $where = ttReportHelper::getExpenseWhere($options);
431       $group_by_expenses_part = ttReportHelper::makeGroupByExpensesPart($options);
432       $sql_for_expenses = "select $concat_part, null as time";
433       if ($options['show_work_units']) $sql_for_expenses .= ", null as units";
434       $sql_for_expenses .= ", sum(ei.cost) as cost, sum(ei.cost) as expenses from tt_expense_items ei $join_part $where $group_by_expenses_part";
435
436       // Create a combined query.
437       $fields = ttReportHelper::makeCombinedSelectPart($options);
438       $combined = "select $fields, sum(time) as time";
439       if ($options['show_work_units']) $combined .= ", sum(units) as units";
440       $combined .= ", sum(cost) as cost, sum(expenses) as expenses from (($sql) union all ($sql_for_expenses)) t group by $fields";
441       $sql = $combined;
442     }
443
444     // Execute query.
445     $res = $mdb2->query($sql);
446     if (is_a($res, 'PEAR_Error')) die($res->getMessage());
447     while ($val = $res->fetchRow()) {
448       $time = $val['time'] ? sec_to_time_fmt_hm($val['time']) : null;
449       $rowLabel = ttReportHelper::makeGroupByLabel($val['group_field'], $options);
450       if ($options['show_cost']) {
451         $decimalMark = $user->getDecimalMark();
452         if ('.' != $decimalMark) {
453           $val['cost'] = str_replace('.', $decimalMark, $val['cost']);
454           $val['expenses'] = str_replace('.', $decimalMark, $val['expenses']);
455         }
456         $subtotals[$val['group_field']] = array('name'=>$rowLabel,'user'=>$val['user'],'project'=>$val['project'],'task'=>$val['task'],'client'=>$val['client'],'cf_1'=>$val['cf_1'],'time'=>$time,'units'=> $val['units'],'cost'=>$val['cost'],'expenses'=>$val['expenses']);
457       } else
458         $subtotals[$val['group_field']] = array('name'=>$rowLabel,'user'=>$val['user'],'project'=>$val['project'],'task'=>$val['task'],'client'=>$val['client'],'cf_1'=>$val['cf_1'],'time'=>$time, 'units'=> $val['units']);
459     }
460
461     return $subtotals;
462   }
463
464   // getTotals calculates total hours and cost for all report items.
465   static function getTotals($options)
466   {
467     global $user;
468     $mdb2 = getConnection();
469
470     $trackingMode = $user->getTrackingMode();
471     $decimalMark = $user->getDecimalMark();
472     $where = ttReportHelper::getWhere($options);
473
474     // Prepare parts.
475     $time_part = "sum(time_to_sec(l.duration)) as time";
476     if ($options['show_work_units']) {
477       $unitTotalsOnly = $user->getConfigOption('unit_totals_only');
478       $firstUnitThreshold = $user->getConfigInt('1st_unit_threshold', 0);
479       $minutesInUnit = $user->getConfigInt('minutes_in_unit', 15);
480       $units_part = $unitTotalsOnly ? ", null as units" : ", sum(if(l.billable = 0 or time_to_sec(l.duration)/60 < $firstUnitThreshold, 0, ceil(time_to_sec(l.duration)/60/$minutesInUnit))) as units";
481     }
482     if ($options['show_cost']) {
483       if (MODE_TIME == $trackingMode)
484         $cost_part = ", sum(cast(l.billable * coalesce(u.rate, 0) * time_to_sec(l.duration)/3600 as decimal(10,2))) as cost, null as expenses";
485       else
486         $cost_part = ", sum(cast(l.billable * coalesce(upb.rate, 0) * time_to_sec(l.duration)/3600 as decimal(10,2))) as cost, null as expenses";
487     } else {
488       $cost_part = ", null as cost, null as expenses";
489     }
490     if ($options['show_cost']) {
491       if (MODE_TIME == $trackingMode) {
492         $left_joins = "left join tt_users u on (l.user_id = u.id)";
493       } else {
494         $left_joins = "left join tt_user_project_binds upb on (l.user_id = upb.user_id and l.project_id = upb.project_id)";
495       }
496     }
497     // Prepare a query for time items.
498     $sql = "select $time_part $units_part $cost_part from tt_log l $left_joins $where";
499
500     // If we have expenses, query becomes a bit more complex.
501     if ($options['show_cost'] && $user->isPluginEnabled('ex')) {
502       $where = ttReportHelper::getExpenseWhere($options);
503       $sql_for_expenses = "select null as time";
504       if ($options['show_work_units']) $sql_for_expenses .= ", null as units";
505       $sql_for_expenses .= ", sum(cost) as cost, sum(cost) as expenses from tt_expense_items ei $where";
506
507       // Create a combined query.
508       $combined = "select sum(time) as time";
509       if ($options['show_work_units']) $combined .= ", sum(units) as units";
510       $combined .= ", sum(cost) as cost, sum(expenses) as expenses from (($sql) union all ($sql_for_expenses)) t";
511       $sql = $combined;
512     }
513
514     // Execute query.
515     $res = $mdb2->query($sql);
516     if (is_a($res, 'PEAR_Error')) die($res->getMessage());
517
518     $val = $res->fetchRow();
519     $total_time = $val['time'] ? sec_to_time_fmt_hm($val['time']) : null;
520     if ($options['show_cost']) {
521       $total_cost = $val['cost'];
522       if (!$total_cost) $total_cost = '0.00';
523       if ('.' != $decimalMark)
524         $total_cost = str_replace('.', $decimalMark, $total_cost);
525       $total_expenses = $val['expenses'];
526       if (!$total_expenses) $total_expenses = '0.00';
527       if ('.' != $decimalMark)
528         $total_expenses = str_replace('.', $decimalMark, $total_expenses);
529     }
530
531     $dateFormat = $user->getDateFormat();
532     if ($options['period'])
533       $period = new Period($options['period'], new DateAndTime($dateFormat));
534     else {
535       $period = new Period();
536       $period->setPeriod(
537         new DateAndTime($dateFormat, $options['period_start']),
538         new DateAndTime($dateFormat, $options['period_end']));
539     }
540
541     $totals['start_date'] = $period->getStartDate();
542     $totals['end_date'] = $period->getEndDate();
543     $totals['time'] = $total_time;
544     $totals['units'] = $val['units'];
545     $totals['cost'] = $total_cost;
546     $totals['expenses'] = $total_expenses;
547
548     return $totals;
549   }
550
551   // The assignToInvoice assigns a set of records to a specific invoice.
552   static function assignToInvoice($invoice_id, $time_log_ids, $expense_item_ids) {
553     global $user;
554     $mdb2 = getConnection();
555
556     $group_id = $user->getGroup();
557     $org_id = $user->org_id;
558
559     if ($time_log_ids) {
560       $sql = "update tt_log set invoice_id = ".$mdb2->quote($invoice_id).
561         " where id in(".join(', ', $time_log_ids).") and group_id = $group_id and org_id = $org_id";
562       $affected = $mdb2->exec($sql);
563       if (is_a($affected, 'PEAR_Error')) die($affected->getMessage());
564     }
565     if ($expense_item_ids) {
566       $sql = "update tt_expense_items set invoice_id = ".$mdb2->quote($invoice_id).
567         " where id in(".join(', ', $expense_item_ids).") and group_id = $group_id and org_id = $org_id";
568       $affected = $mdb2->exec($sql);
569       if (is_a($affected, 'PEAR_Error')) die($affected->getMessage());
570     }
571   }
572
573   // The markPaid marks a set of records as either paid or unpaid.
574   static function markPaid($time_log_ids, $expense_item_ids, $paid = true) {
575     global $user;
576     $mdb2 = getConnection();
577
578     $group_id = $user->getGroup();
579     $org_id = $user->org_id;
580
581     $paid_val = (int) $paid;
582     if ($time_log_ids) {
583       $sql = "update tt_log set paid = $paid_val".
584         " where id in(".join(', ', $time_log_ids).") and group_id = $group_id and org_id = $org_id";
585       $affected = $mdb2->exec($sql);
586       if (is_a($affected, 'PEAR_Error')) die($affected->getMessage());
587     }
588     if ($expense_item_ids) {
589       $sql = "update tt_expense_items set paid = $paid_val".
590         " where id in(".join(', ', $expense_item_ids).") and group_id = $group_id and org_id = $org_id";
591       $affected = $mdb2->exec($sql);
592       if (is_a($affected, 'PEAR_Error')) die($affected->getMessage());
593     }
594   }
595
596   // prepareReportBody - prepares an email body for report.
597   static function prepareReportBody($options, $comment = null)
598   {
599     global $user;
600     global $i18n;
601
602     // Determine these once as they are used in multiple places in this function.
603     $canViewReports = $user->can('view_reports') || $user->can('view_all_reports');
604     $isClient = $user->isClient();
605
606     $items = ttReportHelper::getItems($options);
607     $grouping = ttReportHelper::grouping($options);
608     if ($grouping)
609       $subtotals = ttReportHelper::getSubtotals($options);
610     $totals = ttReportHelper::getTotals($options);
611
612     // Use custom fields plugin if it is enabled.
613     if ($user->isPluginEnabled('cf'))
614       $custom_fields = new CustomFields();
615
616     // Define some styles to use in email.
617     $style_title = 'text-align: center; font-size: 15pt; font-family: Arial, Helvetica, sans-serif;';
618     $tableHeader = 'font-weight: bold; background-color: #a6ccf7; text-align: left;';
619     $tableHeaderCentered = 'font-weight: bold; background-color: #a6ccf7; text-align: center;';
620     $rowItem = 'background-color: #ffffff;';
621     $rowItemAlt = 'background-color: #f5f5f5;';
622     $rowSubtotal = 'background-color: #e0e0e0;';
623     $cellLeftAligned = 'text-align: left; vertical-align: top;';
624     $cellRightAligned = 'text-align: right; vertical-align: top;';
625     $cellLeftAlignedSubtotal = 'font-weight: bold; text-align: left; vertical-align: top;';
626     $cellRightAlignedSubtotal = 'font-weight: bold; text-align: right; vertical-align: top;';
627
628     // Start creating email body.
629     $body = '<html>';
630     $body .= '<head><meta http-equiv="content-type" content="text/html; charset='.CHARSET.'"></head>';
631     $body .= '<body>';
632
633     // Output title.
634     $body .= '<p style="'.$style_title.'">'.$i18n->get('form.mail.report_subject').': '.$totals['start_date'].' - '.$totals['end_date'].'</p>';
635
636     // Output comment.
637     if ($comment) $body .= '<p>'.htmlspecialchars($comment).'</p>';
638
639     if ($options['show_totals_only']) {
640       // Totals only report. Output subtotals.
641       $group_by_header = ttReportHelper::makeGroupByHeader($options);
642
643       $body .= '<table border="0" cellpadding="4" cellspacing="0" width="100%">';
644       $body .= '<tr>';
645       $body .= '<td style="'.$tableHeader.'">'.$group_by_header.'</td>';
646       if ($options['show_duration'])
647         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.duration').'</td>';
648       if ($options['show_work_units'])
649         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.work_units_short').'</td>';
650       if ($options['show_cost'])
651         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.cost').'</td>';
652       $body .= '</tr>';
653       foreach($subtotals as $subtotal) {
654         $body .= '<tr style="'.$rowSubtotal.'">';
655         $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($subtotal['name'] ? htmlspecialchars($subtotal['name']) : '&nbsp;').'</td>';
656         if ($options['show_duration']) {
657           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
658           if ($subtotal['time'] <> '0:00') $body .= $subtotal['time'];
659           $body .= '</td>';
660         }
661         if ($options['show_work_units']) {
662           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
663           $body .= $subtotal['units'];
664           $body .= '</td>';
665         }
666         if ($options['show_cost']) {
667           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
668           $body .= ($canViewReports || $isClient) ? $subtotal['cost'] : $subtotal['expenses'];
669           $body .= '</td>';
670         }
671         $body .= '</tr>';
672       }
673
674       // Print totals.
675       $body .= '<tr><td>&nbsp;</td></tr>';
676       $body .= '<tr style="'.$rowSubtotal.'">';
677       $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.total').'</td>';
678       if ($options['show_duration']) {
679         $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
680         if ($totals['time'] <> '0:00') $body .= $totals['time'];
681         $body .= '</td>';
682       }
683       if ($options['show_work_units']) {
684         $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
685         $body .= $totals['units'];
686         $body .= '</td>';
687       }
688       if ($options['show_cost']) {
689         $body .= '<td nowrap style="'.$cellRightAlignedSubtotal.'">'.htmlspecialchars($user->currency).' ';
690         $body .= ($canViewReports || $isClient) ? $totals['cost'] : $totals['expenses'];
691         $body .= '</td>';
692       }
693       $body .= '</tr>';
694
695       $body .= '</table>';
696     } else {
697       // Regular report.
698
699       // Print table header.
700       $body .= '<table border="0" cellpadding="4" cellspacing="0" width="100%">';
701       $body .= '<tr>';
702       $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.date').'</td>';
703       if ($canViewReports || $isClient)
704         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.user').'</td>';
705       if ($options['show_client'])
706         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.client').'</td>';
707       if ($options['show_project'])
708         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.project').'</td>';
709       if ($options['show_task'])
710         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.task').'</td>';
711       if ($options['show_custom_field_1'])
712         $body .= '<td style="'.$tableHeader.'">'.htmlspecialchars($custom_fields->fields[0]['label']).'</td>';
713       if ($options['show_start'])
714         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.start').'</td>';
715       if ($options['show_end'])
716         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.finish').'</td>';
717       if ($options['show_duration'])
718         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.duration').'</td>';
719       if ($options['show_work_units'])
720         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.work_units_short').'</td>';
721       if ($options['show_note'])
722         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.note').'</td>';
723       if ($options['show_cost'])
724         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.cost').'</td>';
725       if ($options['show_paid'])
726         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.paid').'</td>';
727       if ($options['show_ip'])
728         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.ip').'</td>';
729       if ($options['show_invoice'])
730         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.invoice').'</td>';
731       $body .= '</tr>';
732
733       // Initialize variables to print subtotals.
734       if ($items && $grouping) {
735         $print_subtotals = true;
736         $first_pass = true;
737         $prev_grouped_by = '';
738         $cur_grouped_by = '';
739       }
740       // Initialize variables to alternate color of rows for different dates.
741       $prev_date = '';
742       $cur_date = '';
743       $row_style = $rowItem;
744
745       // Print report items.
746       if (is_array($items)) {
747         foreach ($items as $record) {
748           $cur_date = $record['date'];
749           // Print a subtotal row after a block of grouped items.
750           if ($print_subtotals) {
751             $cur_grouped_by = $record['grouped_by'];
752             if ($cur_grouped_by != $prev_grouped_by && !$first_pass) {
753               $body .= '<tr style="'.$rowSubtotal.'">';
754               $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.subtotal').'</td>';
755               $subtotal_name = htmlspecialchars($subtotals[$prev_grouped_by]['name']);
756               if ($canViewReports || $isClient) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['user'].'</td>';
757               if ($options['show_client']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['client'].'</td>';
758               if ($options['show_project']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['project'].'</td>';
759               if ($options['show_task']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['task'].'</td>';
760               if ($options['show_custom_field_1']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['cf_1'].'</td>';
761               if ($options['show_start']) $body .= '<td></td>';
762               if ($options['show_end']) $body .= '<td></td>';
763               if ($options['show_duration']) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['time'].'</td>';
764               if ($options['show_work_units']) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['units'].'</td>';
765               if ($options['show_note']) $body .= '<td></td>';
766               if ($options['show_cost']) {
767                 $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
768                 $body .= ($canViewReports || $isClient) ? $subtotals[$prev_grouped_by]['cost'] : $subtotals[$prev_grouped_by]['expenses'];
769                 $body .= '</td>';
770               }
771               if ($options['show_paid']) $body .= '<td></td>';
772               if ($options['show_ip']) $body .= '<td></td>';
773               if ($options['show_invoice']) $body .= '<td></td>';
774               $body .= '</tr>';
775               $body .= '<tr><td>&nbsp;</td></tr>';
776             }
777             $first_pass = false;
778           }
779
780           // Print a regular row.
781           if ($cur_date != $prev_date)
782             $row_style = ($row_style == $rowItem) ? $rowItemAlt : $rowItem;
783           $body .= '<tr style="'.$row_style.'">';
784           $body .= '<td style="'.$cellLeftAligned.'">'.$record['date'].'</td>';
785           if ($canViewReports || $isClient)
786             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['user']).'</td>';
787           if ($options['show_client'])
788             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['client']).'</td>';
789           if ($options['show_project'])
790             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['project']).'</td>';
791           if ($options['show_task'])
792             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['task']).'</td>';
793           if ($options['show_custom_field_1'])
794             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['cf_1']).'</td>';
795           if ($options['show_start'])
796             $body .= '<td nowrap style="'.$cellRightAligned.'">'.$record['start'].'</td>';
797           if ($options['show_end'])
798             $body .= '<td nowrap style="'.$cellRightAligned.'">'.$record['finish'].'</td>';
799           if ($options['show_duration'])
800             $body .= '<td style="'.$cellRightAligned.'">'.$record['duration'].'</td>';
801           if ($options['show_work_units'])
802             $body .= '<td style="'.$cellRightAligned.'">'.$record['units'].'</td>';
803           if ($options['show_note'])
804             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['note']).'</td>';
805           if ($options['show_cost'])
806             $body .= '<td style="'.$cellRightAligned.'">'.$record['cost'].'</td>';
807           if ($options['show_paid']) {
808             $body .= '<td style="'.$cellRightAligned.'">';
809             $body .= $record['paid'] == 1 ? $i18n->get('label.yes') : $i18n->get('label.no');
810             $body .= '</td>';
811           }
812           if ($options['show_ip']) {
813             $body .= '<td style="'.$cellRightAligned.'">';
814             $body .= $record['modified'] ? $record['modified_ip'].' '.$record['modified'] : $record['created_ip'].' '.$record['created'];
815             $body .= '</td>';
816           }
817           if ($options['show_invoice'])
818             $body .= '<td style="'.$cellRightAligned.'">'.htmlspecialchars($record['invoice']).'</td>';
819           $body .= '</tr>';
820
821           $prev_date = $record['date'];
822           if ($print_subtotals)
823             $prev_grouped_by = $record['grouped_by'];
824         }
825       }
826
827       // Print a terminating subtotal.
828       if ($print_subtotals) {
829         $body .= '<tr style="'.$rowSubtotal.'">';
830         $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.subtotal').'</td>';
831         $subtotal_name = htmlspecialchars($subtotals[$cur_grouped_by]['name']);
832         if ($canViewReports || $isClient) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['user'].'</td>';
833         if ($options['show_client']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['client'].'</td>';
834         if ($options['show_project']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['project'].'</td>';
835         if ($options['show_task']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['task'].'</td>';
836         if ($options['show_custom_field_1']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['cf_1'].'</td>';
837         if ($options['show_start']) $body .= '<td></td>';
838         if ($options['show_end']) $body .= '<td></td>';
839         if ($options['show_duration']) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$cur_grouped_by]['time'].'</td>';
840         if ($options['show_work_units']) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$cur_grouped_by]['units'].'</td>';
841         if ($options['show_note']) $body .= '<td></td>';
842         if ($options['show_cost']) {
843           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
844           $body .= ($canViewReports || $isClient) ? $subtotals[$cur_grouped_by]['cost'] : $subtotals[$cur_grouped_by]['expenses'];
845           $body .= '</td>';
846         }
847         if ($options['show_paid']) $body .= '<td></td>';
848         if ($options['show_ip']) $body .= '<td></td>';
849         if ($options['show_invoice']) $body .= '<td></td>';
850         $body .= '</tr>';
851       }
852
853       // Print totals.
854       $body .= '<tr><td>&nbsp;</td></tr>';
855       $body .= '<tr style="'.$rowSubtotal.'">';
856       $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.total').'</td>';
857       if ($canViewReports || $isClient) $body .= '<td></td>';
858       if ($options['show_client']) $body .= '<td></td>';
859       if ($options['show_project']) $body .= '<td></td>';
860       if ($options['show_task']) $body .= '<td></td>';
861       if ($options['show_custom_field_1']) $body .= '<td></td>';
862       if ($options['show_start']) $body .= '<td></td>';
863       if ($options['show_end']) $body .= '<td></td>';
864       if ($options['show_duration']) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$totals['time'].'</td>';
865       if ($options['show_work_units']) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$totals['units'].'</td>';
866       if ($options['show_note']) $body .= '<td></td>';
867       if ($options['show_cost']) {
868         $body .= '<td nowrap style="'.$cellRightAlignedSubtotal.'">'.htmlspecialchars($user->currency).' ';
869         $body .= ($canViewReports || $isClient) ? $totals['cost'] : $totals['expenses'];
870         $body .= '</td>';
871       }
872       if ($options['show_paid']) $body .= '<td></td>';
873       if ($options['show_ip']) $body .= '<td></td>';
874       if ($options['show_invoice']) $body .= '<td></td>';
875       $body .= '</tr>';
876
877       $body .= '</table>';
878     }
879
880     // Output footer.
881     if (!defined('REPORT_FOOTER') || !(REPORT_FOOTER == false))
882       $body .= '<p style="text-align: center;">'.$i18n->get('form.mail.footer').'</p>';
883
884     // Finish creating email body.
885     $body .= '</body></html>';
886
887     return $body;
888   }
889
890   // checkFavReportCondition - checks whether it is okay to send fav report.
891   static function checkFavReportCondition($options, $condition)
892   {
893     $items = ttReportHelper::getItems($options);
894
895     $condition = trim(str_replace('count', '', $condition));
896
897     $greater_or_equal = ttStartsWith($condition, '>=');
898     if ($greater_or_equal) $condition = trim(str_replace('>=', '', $condition));
899
900     $less_or_equal = ttStartsWith($condition, '<=');
901     if ($less_or_equal) $condition = trim(str_replace('<=', '', $condition));
902
903     $not_equal = ttStartsWith($condition, '<>');
904     if ($not_equal) $condition = trim(str_replace('<>', '', $condition));
905
906     $greater = ttStartsWith($condition, '>');
907     if ($greater) $condition = trim(str_replace('>', '', $condition));
908
909     $less = ttStartsWith($condition, '<');
910     if ($less) $condition = trim(str_replace('<', '', $condition));
911
912     $equal = ttStartsWith($condition, '=');
913     if ($equal) $condition = trim(str_replace('=', '', $condition));
914
915     $count_required = (int) $condition;
916
917     if ($greater && count($items) > $count_required) return true;
918     if ($greater_or_equal && count($items) >= $count_required) return true;
919     if ($less && count($items) < $count_required) return true;
920     if ($less_or_equal && count($items) <= $count_required) return true;
921     if ($equal && count($items) == $count_required) return true;
922     if ($not_equal && count($items) <> $count_required) return true;
923
924     return false;
925   }
926
927   // sendFavReport - sends a favorite report to a specified email, called from cron.php
928   static function sendFavReport($options, $subject, $email, $cc) {
929     // We are called from cron.php, we have no $bean in session.
930     // cron.php sets global $user and $i18n objects to match our favorite report user.
931     global $user;
932     global $i18n;
933
934     // Prepare report body.
935     $body = ttReportHelper::prepareReportBody($options);
936
937     import('mail.Mailer');
938     $mailer = new Mailer();
939     $mailer->setCharSet(CHARSET);
940     $mailer->setContentType('text/html');
941     $mailer->setSender(SENDER);
942     if (!empty($cc))
943       $mailer->setReceiverCC($cc);
944     if (!empty($user->bcc_email))
945       $mailer->setReceiverBCC($user->bcc_email);
946     $mailer->setReceiver($email);
947     $mailer->setMailMode(MAIL_MODE);
948     if (empty($subject)) $subject = $options['name'];
949     if (!$mailer->send($subject, $body))
950       return false;
951
952     return true;
953   }
954
955   // getReportOptions - returns an array of report options constructed from session bean.
956   //
957   // Note: similarly to ttFavReportHelper::getReportOptions, this function is a part of
958   // refactoring to simplify maintenance of report generating functions, as we currently
959   // have 2 sets: normal reporting (from bean), and fav report emailing (from db fields).
960   // Using options obtained from either db or bean shall allow us to use only one set of functions.
961   static function getReportOptions($bean) {
962     global $user;
963
964     // Prepare an array of report options.
965     $options = array();
966
967     // Construct one by one.
968     $options['name'] = null; // No name required.
969     $options['user_id'] = $user->id; // Not sure if we need user_id here. Fav reports use it to recycle $user object in cron.php.
970     $options['client_id'] = $bean->getAttribute('client');
971     $options['cf_1_option_id'] = $bean->getAttribute('option');
972     $options['project_id'] = $bean->getAttribute('project');
973     $options['task_id'] = $bean->getAttribute('task');
974     $options['billable'] = $bean->getAttribute('include_records');
975     $options['invoice'] = $bean->getAttribute('invoice');
976     $options['paid_status'] = $bean->getAttribute('paid_status');
977     if (is_array($bean->getAttribute('users'))) $options['users'] = join(',', $bean->getAttribute('users'));
978     $options['period'] = $bean->getAttribute('period');
979     $options['period_start'] = $bean->getAttribute('start_date');
980     $options['period_end'] = $bean->getAttribute('end_date');
981     $options['show_client'] = $bean->getAttribute('chclient');
982     $options['show_invoice'] = $bean->getAttribute('chinvoice');
983     $options['show_paid'] = $bean->getAttribute('chpaid');
984     $options['show_ip'] = $bean->getAttribute('chip');
985     $options['show_project'] = $bean->getAttribute('chproject');
986     $options['show_start'] = $bean->getAttribute('chstart');
987     $options['show_duration'] = $bean->getAttribute('chduration');
988     $options['show_cost'] = $bean->getAttribute('chcost');
989     $options['show_task'] = $bean->getAttribute('chtask');
990     $options['show_end'] = $bean->getAttribute('chfinish');
991     $options['show_note'] = $bean->getAttribute('chnote');
992     $options['show_custom_field_1'] = $bean->getAttribute('chcf_1');
993     $options['show_work_units'] = $bean->getAttribute('chunits');
994     $options['show_totals_only'] = $bean->getAttribute('chtotalsonly');
995     $options['group_by1'] = $bean->getAttribute('group_by1');
996     $options['group_by2'] = $bean->getAttribute('group_by2');
997     $options['group_by3'] = $bean->getAttribute('group_by3');
998     return $options;
999   }
1000
1001   // verifyBean is a security function to make sure data in bean makes sense for a group.
1002   static function verifyBean($bean) {
1003     global $user;
1004
1005     // Check users.
1006     $users_in_bean = $bean->getAttribute('users');
1007     if (is_array($users_in_bean)) {
1008       $users_in_group = ttGroupHelper::getUsers();
1009       foreach ($users_in_group as $user_in_group) {
1010         $valid_ids[] = $user_in_group['id'];
1011       }
1012       foreach ($users_in_bean as $user_in_bean) {
1013         if (!in_array($user_in_bean, $valid_ids)) {
1014           return false;
1015         }
1016       }
1017     }
1018
1019     // TODO: add additional checks here. Perhaps do it before saving the bean for consistency.
1020     return true;
1021   }
1022
1023   // makeGroupByKey builds a combined group by key from group_by1, group_by2 and group_by3 values
1024   // (passed in $options) and a row of data ($row obtained from a db query).
1025   static function makeGroupByKey($options, $row) {
1026     if ($options['group_by1'] != null && $options['group_by1'] != 'no_grouping') {
1027       // We have group_by1.
1028       $group_by1 = $options['group_by1'];
1029       $group_by1_value = $row[$group_by1];
1030       //if ($group_by1 == 'date') $group_by1_value = ttDateToUserFormat($group_by1_value);
1031       if (empty($group_by1_value)) $group_by1_value = 'Null'; // To match what comes out of makeConcatPart.
1032       $group_by_key .= ' - '.$group_by1_value;
1033     }
1034     if ($options['group_by2'] != null && $options['group_by2'] != 'no_grouping') {
1035       // We have group_by2.
1036       $group_by2 = $options['group_by2'];
1037       $group_by2_value = $row[$group_by2];
1038       //if ($group_by2 == 'date') $group_by2_value = ttDateToUserFormat($group_by2_value);
1039       if (empty($group_by2_value)) $group_by2_value = 'Null'; // To match what comes out of makeConcatPart.
1040       $group_by_key .= ' - '.$group_by2_value;
1041     }
1042     if ($options['group_by3'] != null && $options['group_by3'] != 'no_grouping') {
1043       // We have group_by3.
1044       $group_by3 = $options['group_by3'];
1045       $group_by3_value = $row[$group_by3];
1046       //if ($group_by3 == 'date') $group_by3_value = ttDateToUserFormat($group_by3_value);
1047       if (empty($group_by3_value)) $group_by3_value = 'Null'; // To match what comes out of makeConcatPart.
1048       $group_by_key .= ' - '.$group_by3_value;
1049     }
1050     $group_by_key = trim($group_by_key, ' -');
1051     return $group_by_key;
1052   }
1053
1054   // makeGroupByPart builds a combined group by part for sql query for time items using group_by1,
1055   // group_by2, and group_by3 values passed in $options.
1056   static function makeGroupByPart($options) {
1057     if (!ttReportHelper::grouping($options)) return null;
1058
1059     $group_by1 = $options['group_by1'];
1060     $group_by2 = $options['group_by2'];
1061     $group_by3 = $options['group_by3'];
1062
1063     switch ($group_by1) {
1064       case 'date':
1065         $group_by_parts .= ', l.date';
1066         break;
1067       case 'user':
1068         $group_by_parts .= ', u.name';
1069         break;
1070       case 'client':
1071         $group_by_parts .= ', c.name';
1072         break;
1073       case 'project':
1074         $group_by_parts .= ', p.name';
1075         break;
1076       case 'task':
1077         $group_by_parts .= ', t.name';
1078         break;
1079       case 'cf_1':
1080         $group_by_parts .= ', cfo.value';
1081         break;
1082     }
1083     switch ($group_by2) {
1084       case 'date':
1085         $group_by_parts .= ', l.date';
1086         break;
1087       case 'user':
1088         $group_by_parts .= ', u.name';
1089         break;
1090       case 'client':
1091         $group_by_parts .= ', c.name';
1092         break;
1093       case 'project':
1094         $group_by_parts .= ', p.name';
1095         break;
1096       case 'task':
1097         $group_by_parts .= ', t.name';
1098         break;
1099       case 'cf_1':
1100         $group_by_parts .= ', cfo.value';
1101         break;
1102     }
1103     switch ($group_by3) {
1104       case 'date':
1105         $group_by_parts .= ', l.date';
1106         break;
1107       case 'user':
1108         $group_by_parts .= ', u.name';
1109         break;
1110       case 'client':
1111         $group_by_parts .= ', c.name';
1112         break;
1113       case 'project':
1114         $group_by_parts .= ', p.name';
1115         break;
1116       case 'task':
1117         $group_by_parts .= ', t.name';
1118         break;
1119       case 'cf_1':
1120         $group_by_parts .= ', cfo.value';
1121         break;
1122     }
1123     // Remove garbage from the beginning.
1124     $group_by_parts = ltrim($group_by_parts, ', ');
1125     $group_by_part = "group by $group_by_parts";
1126     return $group_by_part;
1127   }
1128
1129   // makeGroupByExpensesPart builds a combined group by part for sql query for expense items using
1130   // group_by1, group_by2, and group_by3 values passed in $options.
1131   static function makeGroupByExpensesPart($options) {
1132     $no_grouping = ($options['group_by1'] == null || $options['group_by1'] == 'no_grouping') &&
1133       ($options['group_by2'] == null || $options['group_by2'] == 'no_grouping') &&
1134       ($options['group_by3'] == null || $options['group_by3'] == 'no_grouping');
1135     if ($no_grouping) return null;
1136
1137     $group_by1 = $options['group_by1'];
1138     $group_by2 = $options['group_by2'];
1139     $group_by3 = $options['group_by3'];
1140
1141     switch ($group_by1) {
1142       case 'date':
1143         $group_by_parts .= ', ei.date';
1144         break;
1145       case 'user':
1146         $group_by_parts .= ', u.name';
1147         break;
1148       case 'client':
1149         $group_by_parts .= ', c.name';
1150         break;
1151       case 'project':
1152         $group_by_parts .= ', p.name';
1153         break;
1154     }
1155     switch ($group_by2) {
1156       case 'date':
1157         $group_by_parts .= ', ei.date';
1158         break;
1159       case 'user':
1160         $group_by_parts .= ', u.name';
1161         break;
1162       case 'client':
1163         $group_by_parts .= ', c.name';
1164         break;
1165       case 'project':
1166         $group_by_parts .= ', p.name';
1167         break;
1168     }
1169     switch ($group_by3) {
1170       case 'date':
1171         $group_by_parts .= ', ei.date';
1172         break;
1173       case 'user':
1174         $group_by_parts .= ', u.name';
1175         break;
1176       case 'client':
1177         $group_by_parts .= ', c.name';
1178         break;
1179       case 'project':
1180         $group_by_parts .= ', p.name';
1181         break;
1182     }
1183     // Remove garbage from the beginning.
1184     $group_by_parts = ltrim($group_by_parts, ', ');
1185     if ($group_by_parts)
1186       $group_by_part = "group by $group_by_parts";
1187     return $group_by_part;
1188   }
1189
1190   // makeConcatPart builds a concatenation part for getSubtotals query (for time items).
1191   static function makeConcatPart($options) {
1192     $group_by1 = $options['group_by1'];
1193     $group_by2 = $options['group_by2'];
1194     $group_by3 = $options['group_by3'];
1195
1196     switch ($group_by1) {
1197       case 'date':
1198         $what_to_concat .= ", ' - ', l.date";
1199         break;
1200       case 'user':
1201         $what_to_concat .= ", ' - ', u.name";
1202         $fields_part .= ', u.name as user';
1203         break;
1204       case 'client':
1205         $what_to_concat .= ", ' - ', coalesce(c.name, 'Null')";
1206         $fields_part .= ', c.name as client';
1207         break;
1208       case 'project':
1209         $what_to_concat .= ", ' - ', coalesce(p.name, 'Null')";
1210         $fields_part .= ', p.name as project';
1211         break;
1212       case 'task':
1213         $what_to_concat .= ", ' - ', coalesce(t.name, 'Null')";
1214         $fields_part .= ', t.name as task';
1215         break;
1216       case 'cf_1':
1217         $what_to_concat .= ", ' - ', coalesce(cfo.value, 'Null')";
1218         $fields_part .= ', cfo.value as cf_1';
1219         break;
1220     }
1221     switch ($group_by2) {
1222       case 'date':
1223         $what_to_concat .= ", ' - ', l.date";
1224         break;
1225       case 'user':
1226         $what_to_concat .= ", ' - ', u.name";
1227         $fields_part .= ', u.name as user';
1228         break;
1229       case 'client':
1230         $what_to_concat .= ", ' - ', coalesce(c.name, 'Null')";
1231         $fields_part .= ', c.name as client';
1232         break;
1233       case 'project':
1234         $what_to_concat .= ", ' - ', coalesce(p.name, 'Null')";
1235         $fields_part .= ', p.name as project';
1236         break;
1237       case 'task':
1238         $what_to_concat .= ", ' - ', coalesce(t.name, 'Null')";
1239         $fields_part .= ', t.name as task';
1240         break;
1241       case 'cf_1':
1242         $what_to_concat .= ", ' - ', coalesce(cfo.value, 'Null')";
1243         $fields_part .= ', cfo.value as cf_1';
1244         break;
1245     }
1246     switch ($group_by3) {
1247       case 'date':
1248         $what_to_concat .= ", ' - ', l.date";
1249         break;
1250       case 'user':
1251         $what_to_concat .= ", ' - ', u.name";
1252         $fields_part .= ', u.name as user';
1253         break;
1254       case 'client':
1255         $what_to_concat .= ", ' - ', coalesce(c.name, 'Null')";
1256         $fields_part .= ', c.name as client';
1257         break;
1258       case 'project':
1259         $what_to_concat .= ", ' - ', coalesce(p.name, 'Null')";
1260         $fields_part .= ', p.name as project';
1261         break;
1262       case 'task':
1263         $what_to_concat .= ", ' - ', coalesce(t.name, 'Null')";
1264         $fields_part .= ', t.name as task';
1265         break;
1266       case 'cf_1':
1267         $what_to_concat .= ", ' - ', coalesce(cfo.value, 'Null')";
1268         $fields_part .= ', cfo.value as cf_1';
1269         break;
1270     }
1271     // Remove garbage from both ends.
1272     $what_to_concat = trim($what_to_concat, "', -");
1273     $concat_part = "concat($what_to_concat) as group_field";
1274     $concat_part = trim($concat_part, ' -');
1275     return "$concat_part $fields_part";
1276   }
1277
1278   // makeConcatPart builds a concatenation part for getSubtotals query (for expense items).
1279   static function makeConcatExpensesPart($options) {
1280     $group_by1 = $options['group_by1'];
1281     $group_by2 = $options['group_by2'];
1282     $group_by3 = $options['group_by3'];
1283
1284     switch ($group_by1) {
1285       case 'date':
1286         $what_to_concat .= ", ' - ', ei.date";
1287         break;
1288       case 'user':
1289         $what_to_concat .= ", ' - ', u.name";
1290         $fields_part .= ', u.name as user';
1291         break;
1292       case 'client':
1293         $what_to_concat .= ", ' - ', coalesce(c.name, 'Null')";
1294         $fields_part .= ', c.name as client';
1295         break;
1296       case 'project':
1297         $what_to_concat .= ", ' - ', coalesce(p.name, 'Null')";
1298         $fields_part .= ', p.name as project';
1299         break;
1300
1301       case 'task':
1302         $what_to_concat .= ", ' - ', 'Null'";
1303         $fields_part .= ', null as task';
1304         break;
1305
1306       case 'cf_1':
1307         $what_to_concat .= ", ' - ', 'Null'";
1308         $fields_part .= ', null as cf_1';
1309         break;
1310     }
1311     switch ($group_by2) {
1312       case 'date':
1313         $what_to_concat .= ", ' - ', ei.date";
1314         break;
1315       case 'user':
1316         $what_to_concat .= ", ' - ', u.name";
1317         $fields_part .= ', u.name as user';
1318         break;
1319       case 'client':
1320         $what_to_concat .= ", ' - ', coalesce(c.name, 'Null')";
1321         $fields_part .= ', c.name as client';
1322         break;
1323       case 'project':
1324         $what_to_concat .= ", ' - ', coalesce(p.name, 'Null')";
1325         $fields_part .= ', p.name as project';
1326         break;
1327
1328       case 'task':
1329         $what_to_concat .= ", ' - ', 'Null'";
1330         $fields_part .= ', null as task';
1331         break;
1332
1333       case 'cf_1':
1334         $what_to_concat .= ", ' - ', 'Null'";
1335         $fields_part .= ', null as cf_1';
1336         break;
1337     }
1338     switch ($group_by3) {
1339       case 'date':
1340         $what_to_concat .= ", ' - ', ei.date";
1341         break;
1342       case 'user':
1343         $what_to_concat .= ", ' - ', u.name";
1344         $fields_part .= ', u.name as user';
1345         break;
1346       case 'client':
1347         $what_to_concat .= ", ' - ', coalesce(c.name, 'Null')";
1348         $fields_part .= ', c.name as client';
1349         break;
1350       case 'project':
1351         $what_to_concat .= ", ' - ', coalesce(p.name, 'Null')";
1352         $fields_part .= ', p.name as project';
1353         break;
1354
1355       case 'task':
1356         $what_to_concat .= ", ' - ', 'Null'";
1357         $fields_part .= ', null as task';
1358         break;
1359
1360       case 'cf_1':
1361         $what_to_concat .= ", ' - ', 'Null'";
1362         $fields_part .= ', null as cf_1';
1363         break;
1364     }
1365     // Remove garbage from the beginning.
1366     if ($what_to_concat)
1367         $what_to_concat = substr($what_to_concat, 8);
1368     $concat_part = "concat($what_to_concat) as group_field";
1369     return "$concat_part $fields_part";
1370   }
1371
1372   // makeCombinedSelectPart builds a list of fields for a combined select on a union for getSubtotals.
1373   // This is used when we include expenses.
1374   static function makeCombinedSelectPart($options) {
1375     $group_by1 = $options['group_by1'];
1376     $group_by2 = $options['group_by2'];
1377     $group_by3 = $options['group_by3'];
1378
1379     $fields = "group_field";
1380
1381     switch ($group_by1) {
1382       case 'user':
1383         $fields .= ', user';
1384         break;
1385       case 'client':
1386         $fields_part .= ', client';
1387         break;
1388       case 'project':
1389         $fields .= ', project';
1390         break;
1391
1392       case 'task':
1393         $fields .= ', task';
1394         break;
1395
1396       case 'cf_1':
1397         $fields .= ', cf_1';
1398         break;
1399     }
1400     switch ($group_by2) {
1401       case 'user':
1402         $fields .= ', user';
1403         break;
1404       case 'client':
1405         $fields_part .= ', client';
1406         break;
1407       case 'project':
1408         $fields .= ', project';
1409         break;
1410
1411       case 'task':
1412         $fields .= ', task';
1413         break;
1414
1415       case 'cf_1':
1416         $fields .= ', cf_1';
1417         break;
1418     }
1419     switch ($group_by3) {
1420       case 'user':
1421         $fields .= ', user';
1422         break;
1423       case 'client':
1424         $fields_part .= ', client';
1425         break;
1426       case 'project':
1427         $fields .= ', project';
1428         break;
1429
1430       case 'task':
1431         $fields .= ', task';
1432         break;
1433
1434       case 'cf_1':
1435         $fields .= ', cf_1';
1436         break;
1437     }
1438     return $fields;
1439   }
1440
1441   // makeJoinPart builds a left join part for getSubtotals query (for time items).
1442   static function makeJoinPart($options) {
1443     global $user;
1444
1445     $trackingMode = $user->getTrackingMode();
1446     if (ttReportHelper::groupingBy('user', $options) || MODE_TIME == $trackingMode) {
1447       $join .= ' left join tt_users u on (l.user_id = u.id)';
1448     }
1449     if (ttReportHelper::groupingBy('client', $options)) {
1450       $join .= ' left join tt_clients c on (l.client_id = c.id)';
1451     }
1452     if (ttReportHelper::groupingBy('project', $options)) {
1453       $join .= ' left join tt_projects p on (l.project_id = p.id)';
1454     }
1455     if (ttReportHelper::groupingBy('task', $options)) {
1456       $join .= ' left join tt_tasks t on (l.task_id = t.id)';
1457     }
1458     if (ttReportHelper::groupingBy('cf_1', $options)) {
1459       $custom_fields = new CustomFields();
1460       if ($custom_fields->fields[0]['type'] == CustomFields::TYPE_TEXT)
1461         $join .= ' left join tt_custom_field_log cfl on (l.id = cfl.log_id and cfl.status = 1) left join tt_custom_field_options cfo on (cfl.value = cfo.id)';
1462       elseif ($custom_fields->fields[0]['type'] == CustomFields::TYPE_DROPDOWN)
1463         $join .= ' left join tt_custom_field_log cfl on (l.id = cfl.log_id and cfl.status = 1) left join tt_custom_field_options cfo on (cfl.option_id = cfo.id)';
1464     }
1465     if ($options['show_cost'] && $trackingMode != MODE_TIME) {
1466       $join .= ' left join tt_user_project_binds upb on (l.user_id = upb.user_id and l.project_id = upb.project_id)';
1467     }
1468     return $join;
1469   }
1470
1471   // makeWorkUnitPart builds an sql part for work units for time items.
1472   static function makeWorkUnitPart($options) {
1473     global $user;
1474
1475     $workUnits = $options['show_work_units'];
1476     if ($workUnits) {
1477       $unitTotalsOnly = $user->getConfigOption('unit_totals_only');
1478       $firstUnitThreshold = $user->getConfigInt('1st_unit_threshold', 0);
1479       $minutesInUnit = $user->getConfigInt('minutes_in_unit', 15);
1480       if ($unitTotalsOnly)
1481         $work_unit_part = ", if (sum(l.billable * time_to_sec(l.duration)/60) < $firstUnitThreshold, 0, ceil(sum(l.billable * time_to_sec(l.duration)/60/$minutesInUnit))) as units";
1482       else
1483         $work_unit_part = ", sum(if(l.billable = 0 or time_to_sec(l.duration)/60 < $firstUnitThreshold, 0, ceil(time_to_sec(l.duration)/60/$minutesInUnit))) as units";
1484     }
1485     return $work_unit_part;
1486   }
1487
1488   // makeCostPart builds a cost part for time items.
1489   static function makeCostPart($options) {
1490     global $user;
1491
1492     if ($options['show_cost']) {
1493       if (MODE_TIME == $user->getTrackingMode())
1494         $cost_part = ", sum(cast(l.billable * coalesce(u.rate, 0) * time_to_sec(l.duration)/3600 as decimal(10, 2))) as cost";
1495       else
1496         $cost_part .= ", sum(cast(l.billable * coalesce(upb.rate, 0) * time_to_sec(l.duration)/3600 as decimal(10,2))) as cost";
1497     }
1498     return $cost_part;
1499   }
1500
1501   // makeJoinExpensesPart builds a left join part for getSubtotals query for expense items.
1502   static function makeJoinExpensesPart($options) {
1503     if (ttReportHelper::groupingBy('user', $options)) {
1504       $join .= ' left join tt_users u on (ei.user_id = u.id)';
1505     }
1506     if (ttReportHelper::groupingBy('client', $options)) {
1507       $join .= ' left join tt_clients c on (ei.client_id = c.id)';
1508     }
1509     if (ttReportHelper::groupingBy('project', $options)) {
1510       $join .= ' left join tt_projects p on (ei.project_id = p.id)';
1511     }
1512     return $join;
1513   }
1514
1515   // grouping determines if we are grouping the report by either group_by1,
1516   // group_by2, or group_by3 values passed in $options.
1517   static function grouping($options) {
1518     $grouping = ($options['group_by1'] != null && $options['group_by1'] != 'no_grouping') ||
1519       ($options['group_by2'] != null && $options['group_by2'] != 'no_grouping') ||
1520       ($options['group_by3'] != null && $options['group_by3'] != 'no_grouping');
1521     return $grouping;
1522   }
1523
1524   // groupingBy determines if we are grouping a report by a value of $what
1525   // ('date', 'user', 'project', etc.) by checking group_by1, group_by2,
1526   // and group_by3 values passed in $options.
1527   static function groupingBy($what, $options) {
1528     $grouping = ($options['group_by1'] == $what) || ($options['group_by2'] == $what) || ($options['group_by3'] == $what);
1529     return $grouping;
1530   }
1531
1532   // makeGroupByHeader builds a column header for a totals-only report using group_by1,
1533   // group_by2, and group_by3 values passed in $options.
1534   static function makeGroupByHeader($options) {
1535     global $i18n;
1536     global $custom_fields;
1537
1538     $no_grouping = ($options['group_by1'] == null || $options['group_by1'] == 'no_grouping') &&
1539       ($options['group_by2'] == null || $options['group_by2'] == 'no_grouping') &&
1540       ($options['group_by3'] == null || $options['group_by3'] == 'no_grouping');
1541     if ($no_grouping) return null;
1542
1543     if ($options['group_by1'] != null && $options['group_by1'] != 'no_grouping') {
1544       // We have group_by1.
1545       $group_by1 = $options['group_by1'];
1546       if ('cf_1' == $group_by1)
1547         $group_by_header .= ' - '.$custom_fields->fields[0]['label'];
1548       else {
1549         $key = 'label.'.$group_by1;
1550         $group_by_header .= ' - '.$i18n->get($key);
1551       }
1552     }
1553     if ($options['group_by2'] != null && $options['group_by2'] != 'no_grouping') {
1554       // We have group_by2.
1555       $group_by2 = $options['group_by2'];
1556       if ('cf_1' == $group_by2)
1557         $group_by_header .= ' - '.$custom_fields->fields[0]['label'];
1558       else {
1559         $key = 'label.'.$group_by2;
1560         $group_by_header .= ' - '.$i18n->get($key);
1561       }
1562     }
1563     if ($options['group_by3'] != null && $options['group_by3'] != 'no_grouping') {
1564       // We have group_by3.
1565       $group_by3 = $options['group_by3'];
1566       if ('cf_1' == $group_by3)
1567         $group_by_header .= ' - '.$custom_fields->fields[0]['label'];
1568       else {
1569         $key = 'label.'.$group_by3;
1570         $group_by_header .= ' - '.$i18n->get($key);
1571       }
1572     }
1573     $group_by_header = ltrim($group_by_header, ' -');
1574     return $group_by_header;
1575   }
1576
1577   // makeGroupByXmlTag creates an xml tag for a totals only report using group_by1,
1578   // group_by2, and group_by3 values passed in $options.
1579   static function makeGroupByXmlTag($options) {
1580     if ($options['group_by1'] != null && $options['group_by1'] != 'no_grouping') {
1581       // We have group_by1.
1582       $tag .= '_'.$options['group_by1'];
1583     }
1584     if ($options['group_by2'] != null && $options['group_by2'] != 'no_grouping') {
1585       // We have group_by2.
1586       $tag .= '_'.$options['group_by2'];
1587     }
1588     if ($options['group_by3'] != null && $options['group_by3'] != 'no_grouping') {
1589       // We have group_by3.
1590       $tag .= '_'.$options['group_by3'];
1591     }
1592     $tag = ltrim($tag, '_');
1593     return $tag;
1594   }
1595
1596   // makeGroupByLabel builds a label for one row in a "Totals only" report of grouped by items.
1597   // It does one thing: if we are grouping by date, the date format is converted for user.
1598   static function makeGroupByLabel($key, $options) {
1599     if (!ttReportHelper::groupingBy('date', $options))
1600       return $key; // No need to format.
1601
1602     global $user;
1603     if ($user->getDateFormat() == DB_DATEFORMAT)
1604       return $key; // No need to format.
1605
1606     $label = $key;
1607     if (preg_match('/\d\d\d\d-\d\d-\d\d/', $key, $matches)) {
1608       // Replace the first found match of a date in DB_DATEFORMAT.
1609       // This is not entirely clean but better than nothing for a label in a row.
1610       $userDate = ttDateToUserFormat($matches[0]);
1611       $label = str_replace($matches[0], $userDate, $key);
1612     }
1613     return $label;
1614   }
1615 }