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