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