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