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