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