Added work units in output for emailed reports.
[timetracker.git] / WEB-INF / lib / ttReportHelper.class.php
1 <?php
2 // +----------------------------------------------------------------------+
3 // | Anuko Time Tracker
4 // +----------------------------------------------------------------------+
5 // | Copyright (c) Anuko International Ltd. (https://www.anuko.com)
6 // +----------------------------------------------------------------------+
7 // | LIBERAL FREEWARE LICENSE: This source code document may be used
8 // | by anyone for any purpose, and freely redistributed alone or in
9 // | combination with other software, provided that the license is obeyed.
10 // |
11 // | There are only two ways to violate the license:
12 // |
13 // | 1. To redistribute this code in source form, with the copyright
14 // |    notice or license removed or altered. (Distributing in compiled
15 // |    forms without embedded copyright notices is permitted).
16 // |
17 // | 2. To redistribute modified versions of this code in *any* form
18 // |    that bears insufficient indications that the modifications are
19 // |    not the work of the original author(s).
20 // |
21 // | This license applies to this document only, not any other software
22 // | that it may be combined with.
23 // |
24 // +----------------------------------------------------------------------+
25 // | Contributors:
26 // | https://www.anuko.com/time_tracker/credits.htm
27 // +----------------------------------------------------------------------+
28
29 import('ttClientHelper');
30 import('DateAndTime');
31 import('Period');
32 import('ttTimeHelper');
33
34 require_once(dirname(__FILE__).'/../../plugins/CustomFields.class.php');
35
36 // Class ttReportHelper is used for help with reports.
37 class ttReportHelper {
38
39   // getWhere prepares a WHERE clause for a report query.
40   static function getWhere($bean) {
41     global $user;
42
43     // Prepare dropdown parts.
44     $dropdown_parts = '';
45     if ($bean->getAttribute('client'))
46       $dropdown_parts .= ' and l.client_id = '.$bean->getAttribute('client');
47     elseif ($user->isClient() && $user->client_id)
48       $dropdown_parts .= ' and l.client_id = '.$user->client_id;
49     if ($bean->getAttribute('option')) $dropdown_parts .= ' and l.id in(select log_id from tt_custom_field_log where status = 1 and option_id = '.$bean->getAttribute('option').')';
50     if ($bean->getAttribute('project')) $dropdown_parts .= ' and l.project_id = '.$bean->getAttribute('project');
51     if ($bean->getAttribute('task')) $dropdown_parts .= ' and l.task_id = '.$bean->getAttribute('task');
52     if ($bean->getAttribute('include_records')=='1') $dropdown_parts .= ' and l.billable = 1';
53     if ($bean->getAttribute('include_records')=='2') $dropdown_parts .= ' and l.billable = 0';
54     if ($bean->getAttribute('invoice')=='1') $dropdown_parts .= ' and l.invoice_id is not NULL';
55     if ($bean->getAttribute('invoice')=='2') $dropdown_parts .= ' and l.invoice_id is NULL';
56     if ($bean->getAttribute('paid_status')=='1') $dropdown_parts .= ' and l.paid = 1';
57     if ($bean->getAttribute('paid_status')=='2') $dropdown_parts .= ' and l.paid = 0';
58
59     // Prepare user list part.
60     $userlist = -1;
61     if (($user->can('view_reports') || $user->isClient()) && is_array($bean->getAttribute('users')))
62       $userlist = join(',', $bean->getAttribute('users'));
63     // Prepare sql query part for user list.
64     $user_list_part = null;
65     if ($user->can('view_reports') || $user->isClient())
66       $user_list_part = " and l.user_id in ($userlist)";
67     else
68       $user_list_part = " and l.user_id = ".$user->id;
69
70     // Prepare sql query part for where.
71     if ($bean->getAttribute('period'))
72       $period = new Period($bean->getAttribute('period'), new DateAndTime($user->date_format));
73     else {
74       $period = new Period();
75       $period->setPeriod(
76         new DateAndTime($user->date_format, $bean->getAttribute('start_date')),
77         new DateAndTime($user->date_format, $bean->getAttribute('end_date')));
78     }
79     $where = " where l.status = 1 and l.date >= '".$period->getStartDate(DB_DATEFORMAT)."' and l.date <= '".$period->getEndDate(DB_DATEFORMAT)."'".
80       " $user_list_part $dropdown_parts";
81     return $where;
82   }
83
84   // getFavWhere prepares a WHERE clause for a favorite report query.
85   static function getFavWhere($report) {
86     global $user;
87
88     // Prepare dropdown parts.
89     $dropdown_parts = '';
90     if ($report['client_id'])
91       $dropdown_parts .= ' and l.client_id = '.$report['client_id'];
92     elseif ($user->isClient() && $user->client_id)
93       $dropdown_parts .= ' and l.client_id = '.$user->client_id;
94     if ($report['cf_1_option_id']) $dropdown_parts .= ' and l.id in(select log_id from tt_custom_field_log where status = 1 and option_id = '.$report['cf_1_option_id'].')';
95     if ($report['project_id']) $dropdown_parts .= ' and l.project_id = '.$report['project_id'];
96     if ($report['task_id']) $dropdown_parts .= ' and l.task_id = '.$report['task_id'];
97     if ($report['billable']=='1') $dropdown_parts .= ' and l.billable = 1';
98     if ($report['billable']=='2') $dropdown_parts .= ' and l.billable = 0';
99     if ($report['invoice']=='1') $dropdown_parts .= ' and l.invoice_id is not NULL';
100     if ($report['invoice']=='2') $dropdown_parts .= ' and l.invoice_id is NULL';
101     if ($report['paid_status']=='1') $dropdown_parts .= ' and l.paid = 1';
102     if ($report['paid_status']=='2') $dropdown_parts .= ' and l.paid = 0';
103
104     // Prepare user list part.
105     $userlist = -1;
106     if (($user->can('view_reports') || $user->isClient())) {
107       if ($report['users'])
108         $userlist = $report['users'];
109       else {
110         $active_users = ttTeamHelper::getActiveUsers();
111         foreach ($active_users as $single_user)
112           $users[] = $single_user['id'];
113         $userlist = join(',', $users);
114       }
115     }
116     // Prepare sql query part for user list.
117     $user_list_part = null;
118     if ($user->can('view_reports') || $user->isClient())
119       $user_list_part = " and l.user_id in ($userlist)";
120     else
121       $user_list_part = " and l.user_id = ".$user->id;
122
123     // Prepare sql query part for where.
124     if ($report['period'])
125       $period = new Period($report['period'], new DateAndTime($user->date_format));
126     else {
127       $period = new Period();
128       $period->setPeriod(
129         new DateAndTime($user->date_format, $report['period_start']),
130         new DateAndTime($user->date_format, $report['period_end']));
131     }
132     $where = " where l.status = 1 and l.date >= '".$period->getStartDate(DB_DATEFORMAT)."' and l.date <= '".$period->getEndDate(DB_DATEFORMAT)."'".
133       " $user_list_part $dropdown_parts";
134     return $where;
135   }
136
137   // getExpenseWhere prepares WHERE clause for expenses query in a report.
138   static function getExpenseWhere($bean) {
139     global $user;
140
141     // Prepare dropdown parts.
142     $dropdown_parts = '';
143     if ($bean->getAttribute('client'))
144       $dropdown_parts .= ' and ei.client_id = '.$bean->getAttribute('client');
145     elseif ($user->isClient() && $user->client_id)
146       $dropdown_parts .= ' and ei.client_id = '.$user->client_id;
147     if ($bean->getAttribute('project')) $dropdown_parts .= ' and ei.project_id = '.$bean->getAttribute('project');
148     if ($bean->getAttribute('invoice')=='1') $dropdown_parts .= ' and ei.invoice_id is not NULL';
149     if ($bean->getAttribute('invoice')=='2') $dropdown_parts .= ' and ei.invoice_id is NULL';
150     if ($bean->getAttribute('paid_status')=='1') $dropdown_parts .= ' and ei.paid = 1';
151     if ($bean->getAttribute('paid_status')=='2') $dropdown_parts .= ' and ei.paid = 0';
152
153     // Prepare user list part.
154     $userlist = -1;
155     if (($user->can('view_reports') || $user->isClient()) && is_array($bean->getAttribute('users')))
156       $userlist = join(',', $bean->getAttribute('users'));
157     // Prepare sql query part for user list.
158     $user_list_part = null;
159     if ($user->can('view_reports') || $user->isClient())
160       $user_list_part = " and ei.user_id in ($userlist)";
161     else
162       $user_list_part = " and ei.user_id = ".$user->id;
163
164     // Prepare sql query part for where.
165     if ($bean->getAttribute('period'))
166       $period = new Period($bean->getAttribute('period'), new DateAndTime($user->date_format));
167     else {
168       $period = new Period();
169       $period->setPeriod(
170         new DateAndTime($user->date_format, $bean->getAttribute('start_date')),
171         new DateAndTime($user->date_format, $bean->getAttribute('end_date')));
172     }
173     $where = " where ei.status = 1 and ei.date >= '".$period->getStartDate(DB_DATEFORMAT)."' and ei.date <= '".$period->getEndDate(DB_DATEFORMAT)."'".
174       " $user_list_part $dropdown_parts";
175     return $where;
176   }
177
178   // getFavExpenseWhere prepares a WHERE clause for expenses query in a favorite report.
179   static function getFavExpenseWhere($report) {
180     global $user;
181
182     // Prepare dropdown parts.
183     $dropdown_parts = '';
184     if ($report['client_id'])
185       $dropdown_parts .= ' and ei.client_id = '.$report['client_id'];
186     elseif ($user->isClient() && $user->client_id)
187       $dropdown_parts .= ' and ei.client_id = '.$user->client_id;
188     if ($report['project_id']) $dropdown_parts .= ' and ei.project_id = '.$report['project_id'];
189     if ($report['invoice']=='1') $dropdown_parts .= ' and ei.invoice_id is not NULL';
190     if ($report['invoice']=='2') $dropdown_parts .= ' and ei.invoice_id is NULL';
191     if ($report['paid_status']=='1') $dropdown_parts .= ' and ei.paid = 1';
192     if ($report['paid_status']=='2') $dropdown_parts .= ' and ei.paid = 0';
193
194     // Prepare user list part.
195     $userlist = -1;
196     if (($user->can('view_reports') || $user->isClient())) {
197       if ($report['users'])
198         $userlist = $report['users'];
199       else {
200         $active_users = ttTeamHelper::getActiveUsers();
201         foreach ($active_users as $single_user)
202           $users[] = $single_user['id'];
203         $userlist = join(',', $users);
204       }
205     }
206     // Prepare sql query part for user list.
207     $user_list_part = null;
208     if ($user->can('view_reports') || $user->isClient())
209       $user_list_part = " and ei.user_id in ($userlist)";
210     else
211       $user_list_part = " and ei.user_id = ".$user->id;
212
213     // Prepare sql query part for where.
214     if ($report['period'])
215       $period = new Period($report['period'], new DateAndTime($user->date_format));
216     else {
217       $period = new Period();
218       $period->setPeriod(
219         new DateAndTime($user->date_format, $report['period_start']),
220         new DateAndTime($user->date_format, $report['period_end']));
221     }
222     $where = " where ei.status = 1 and ei.date >= '".$period->getStartDate(DB_DATEFORMAT)."' and ei.date <= '".$period->getEndDate(DB_DATEFORMAT)."'".
223       " $user_list_part $dropdown_parts";
224     return $where;
225   }
226
227   // getItems retrieves all items associated with a report.
228   // It combines tt_log and tt_expense_items in one array for presentation in one table using mysql union all.
229   // Expense items use the "note" field for item name.
230   static function getItems($bean) {
231     global $user;
232     $mdb2 = getConnection();
233
234     // Determine these once as they are used in multiple places in this function.
235     $canViewReports = $user->can('view_reports');
236     $isClient = $user->isClient();
237
238     $group_by_option = $bean->getAttribute('group_by');
239     $convertTo12Hour = ('%I:%M %p' == $user->time_format) && ($bean->getAttribute('chstart') || $bean->getAttribute('chfinish'));
240
241     // Prepare a query for time items in tt_log table.
242     $fields = array(); // An array of fields for database query.
243     array_push($fields, 'l.id as id');
244     array_push($fields, '1 as type'); // Type 1 is for tt_log entries.
245     array_push($fields, 'l.date as date');
246     if($canViewReports || $isClient)
247       array_push($fields, 'u.name as user');
248     // Add client name if it is selected.
249     if ($bean->getAttribute('chclient') || 'client' == $group_by_option)
250       array_push($fields, 'c.name as client');
251     // Add project name if it is selected.
252     if ($bean->getAttribute('chproject') || 'project' == $group_by_option)
253       array_push($fields, 'p.name as project');
254     // Add task name if it is selected.
255     if ($bean->getAttribute('chtask') || 'task' == $group_by_option)
256       array_push($fields, 't.name as task');
257     // Add custom field.
258     $include_cf_1 = $bean->getAttribute('chcf_1') || 'cf_1' == $group_by_option;
259     if ($include_cf_1) {
260       $custom_fields = new CustomFields($user->group_id);
261       $cf_1_type = $custom_fields->fields[0]['type'];
262       if ($cf_1_type == CustomFields::TYPE_TEXT) {
263         array_push($fields, 'cfl.value as cf_1');
264       } elseif ($cf_1_type == CustomFields::TYPE_DROPDOWN) {
265         array_push($fields, 'cfo.value as cf_1');
266       }
267     }
268     // Add start time.
269     if ($bean->getAttribute('chstart')) {
270       array_push($fields, "l.start as unformatted_start");
271       array_push($fields, "TIME_FORMAT(l.start, '%k:%i') as start");
272     }
273     // Add finish time.
274     if ($bean->getAttribute('chfinish'))
275       array_push($fields, "TIME_FORMAT(sec_to_time(time_to_sec(l.start) + time_to_sec(l.duration)), '%k:%i') as finish");
276     // Add duration.
277     if ($bean->getAttribute('chduration'))
278       array_push($fields, "TIME_FORMAT(l.duration, '%k:%i') as duration");
279     // Add 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(l.billable = 0 or time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit)) as units");
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(l.billable = 0 or  time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,
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(l.billable = 0 or  time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,
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(l.billable = 0 or time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,
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(l.billable = 0 or time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,
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(l.billable = 0 or  time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,
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(l.billable = 0 or time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,"
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(if(l.billable = 0 or time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,
1082           sum(cast(l.billable * coalesce(u.rate, 0) * time_to_sec(l.duration)/3600 as decimal(10,2))) as cost,
1083           null as expenses 
1084           from tt_log l
1085           left join tt_users u on (l.user_id = u.id) $where";
1086       } else {
1087         $sql = "select sum(time_to_sec(l.duration)) as time,
1088           sum(if(l.billable = 0 or time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,
1089           sum(cast(l.billable * coalesce(upb.rate, 0) * time_to_sec(l.duration)/3600 as decimal(10,2))) as cost,
1090           null as expenses
1091           from tt_log l
1092           left join tt_user_project_binds upb on (l.user_id = upb.user_id and l.project_id = upb.project_id) $where";
1093       }
1094     } else
1095       $sql = "select sum(time_to_sec(l.duration)) as time,
1096         sum(if(l.billable = 0 or time_to_sec(l.duration)/60 < $user->first_unit_threshold, 0, ceil(time_to_sec(l.duration)/60/$user->minutes_in_unit))) as units,
1097         null as cost, null as expenses from tt_log l $where";
1098
1099     // If we have expenses, query becomes a bit more complex.
1100     if ($report['show_cost'] && $user->isPluginEnabled('ex')) {
1101       $where = ttReportHelper::getFavExpenseWhere($report);
1102       $sql_for_expenses = "select null as time, null as units, sum(cost) as cost, sum(cost) as expenses from tt_expense_items ei $where";
1103       // Create a combined query.
1104       $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";
1105     }
1106
1107     // Execute query.
1108     $res = $mdb2->query($sql);
1109     if (is_a($res, 'PEAR_Error')) die($res->getMessage());
1110
1111     $val = $res->fetchRow();
1112     $total_time = $val['time'] ? sec_to_time_fmt_hm($val['time']) : null;
1113     if ($report['show_cost']) {
1114       $total_cost = $val['cost'];
1115       if (!$total_cost) $total_cost = '0.00';
1116       if ('.' != $user->decimal_mark)
1117         $total_cost = str_replace('.', $user->decimal_mark, $total_cost);
1118       $total_expenses = $val['expenses'];
1119       if (!$total_expenses) $total_expenses = '0.00';
1120       if ('.' != $user->decimal_mark)
1121         $total_expenses = str_replace('.', $user->decimal_mark, $total_expenses);
1122     }
1123
1124     if ($report['period'])
1125       $period = new Period($report['period'], new DateAndTime($user->date_format));
1126     else {
1127       $period = new Period();
1128       $period->setPeriod(
1129         new DateAndTime($user->date_format, $report['period_start']),
1130         new DateAndTime($user->date_format, $report['period_end']));
1131     }
1132
1133     $totals['start_date'] = $period->getStartDate();
1134     $totals['end_date'] = $period->getEndDate();
1135     $totals['time'] = $total_time;
1136     $totals['units'] = $val['units'];
1137     $totals['cost'] = $total_cost;
1138     $totals['expenses'] = $total_expenses;
1139
1140     return $totals;
1141   }
1142
1143   // The assignToInvoice assigns a set of records to a specific invoice.
1144   static function assignToInvoice($invoice_id, $time_log_ids, $expense_item_ids)
1145   {
1146     $mdb2 = getConnection();
1147     if ($time_log_ids) {
1148       $sql = "update tt_log set invoice_id = ".$mdb2->quote($invoice_id).
1149         " where id in(".join(', ', $time_log_ids).")";
1150       $affected = $mdb2->exec($sql);
1151       if (is_a($affected, 'PEAR_Error')) die($affected->getMessage());
1152     }
1153     if ($expense_item_ids) {
1154       $sql = "update tt_expense_items set invoice_id = ".$mdb2->quote($invoice_id).
1155         " where id in(".join(', ', $expense_item_ids).")";
1156       $affected = $mdb2->exec($sql);
1157       if (is_a($affected, 'PEAR_Error')) die($affected->getMessage());
1158     }
1159   }
1160
1161   // The markPaid marks a set of records as either paid or unpaid.
1162   static function markPaid($time_log_ids, $expense_item_ids, $paid = true)
1163   {
1164     $mdb2 = getConnection();
1165     $paid_val = (int) $paid;
1166     if ($time_log_ids) {
1167       $sql = "update tt_log set paid = $paid_val where id in(".join(', ', $time_log_ids).")";
1168       $affected = $mdb2->exec($sql);
1169       if (is_a($affected, 'PEAR_Error')) die($affected->getMessage());
1170     }
1171     if ($expense_item_ids) {
1172       $sql = "update tt_expense_items set paid = $paid_val where id in(".join(', ', $expense_item_ids).")";
1173       $affected = $mdb2->exec($sql);
1174       if (is_a($affected, 'PEAR_Error')) die($affected->getMessage());
1175     }
1176   }
1177
1178   // prepareReportBody - prepares an email body for report.
1179   static function prepareReportBody($bean, $comment)
1180   {
1181     global $user;
1182     global $i18n;
1183
1184     // Determine these once as they are used in multiple places in this function.
1185     $canViewReports = $user->can('view_reports');
1186     $isClient = $user->isClient();
1187
1188     $items = ttReportHelper::getItems($bean);
1189     $group_by = $bean->getAttribute('group_by');
1190     if ($group_by && 'no_grouping' != $group_by)
1191       $subtotals = ttReportHelper::getSubtotals($bean);
1192     $totals = ttReportHelper::getTotals($bean);
1193
1194     // Use custom fields plugin if it is enabled.
1195     if ($user->isPluginEnabled('cf'))
1196       $custom_fields = new CustomFields($user->group_id);
1197
1198     // Define some styles to use in email.
1199     $style_title = 'text-align: center; font-size: 15pt; font-family: Arial, Helvetica, sans-serif;';
1200     $tableHeader = 'font-weight: bold; background-color: #a6ccf7; text-align: left;';
1201     $tableHeaderCentered = 'font-weight: bold; background-color: #a6ccf7; text-align: center;';
1202     $rowItem = 'background-color: #ffffff;';
1203     $rowItemAlt = 'background-color: #f5f5f5;';
1204     $rowSubtotal = 'background-color: #e0e0e0;';
1205     $cellLeftAligned = 'text-align: left; vertical-align: top;';
1206     $cellRightAligned = 'text-align: right; vertical-align: top;';
1207     $cellLeftAlignedSubtotal = 'font-weight: bold; text-align: left; vertical-align: top;';
1208     $cellRightAlignedSubtotal = 'font-weight: bold; text-align: right; vertical-align: top;';
1209
1210     // Start creating email body.
1211     $body = '<html>';
1212     $body .= '<head><meta http-equiv="content-type" content="text/html; charset='.CHARSET.'"></head>';
1213     $body .= '<body>';
1214
1215     // Output title.
1216     $body .= '<p style="'.$style_title.'">'.$i18n->get('form.mail.report_subject').': '.$totals['start_date'].' - '.$totals['end_date'].'</p>';
1217
1218     // Output comment.
1219     if ($comment) $body .= '<p>'.htmlspecialchars($comment).'</p>';
1220
1221     if ($bean->getAttribute('chtotalsonly')) {
1222       // Totals only report. Output subtotals.
1223
1224       // Determine group_by header.
1225       if ('cf_1' == $group_by)
1226         $group_by_header = htmlspecialchars($custom_fields->fields[0]['label']);
1227       else {
1228         $key = 'label.'.$group_by;
1229         $group_by_header = $i18n->get($key);
1230       }
1231
1232       $body .= '<table border="0" cellpadding="4" cellspacing="0" width="100%">';
1233       $body .= '<tr>';
1234       $body .= '<td style="'.$tableHeader.'">'.$group_by_header.'</td>';
1235       if ($bean->getAttribute('chduration'))
1236         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.duration').'</td>';
1237       if ($bean->getAttribute('chunits'))
1238         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.work_units_short').'</td>';
1239       if ($bean->getAttribute('chcost'))
1240         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.cost').'</td>';
1241       $body .= '</tr>';
1242       foreach($subtotals as $subtotal) {
1243         $body .= '<tr style="'.$rowSubtotal.'">';
1244         $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($subtotal['name'] ? htmlspecialchars($subtotal['name']) : '&nbsp;').'</td>';
1245         if ($bean->getAttribute('chduration')) {
1246           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1247           if ($subtotal['time'] <> '0:00') $body .= $subtotal['time'];
1248           $body .= '</td>';
1249         }
1250         if ($bean->getAttribute('chunits')) {
1251           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1252           $body .= $subtotal['units'];
1253           $body .= '</td>';
1254         }
1255         if ($bean->getAttribute('chcost')) {
1256           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1257           $body .= ($canViewReports || $isClient) ? $subtotal['cost'] : $subtotal['expenses'];
1258           $body .= '</td>';
1259         }
1260         $body .= '</tr>';
1261       }
1262
1263       // Print totals.
1264       $body .= '<tr><td>&nbsp;</td></tr>';
1265       $body .= '<tr style="'.$rowSubtotal.'">';
1266       $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.total').'</td>';
1267       if ($bean->getAttribute('chduration')) {
1268         $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1269         if ($totals['time'] <> '0:00') $body .= $totals['time'];
1270         $body .= '</td>';
1271       }
1272       if ($bean->getAttribute('chunits')) {
1273         $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1274         $body .= $totals['units'];
1275         $body .= '</td>';
1276       }
1277       if ($bean->getAttribute('chcost')) {
1278         $body .= '<td nowrap style="'.$cellRightAlignedSubtotal.'">'.htmlspecialchars($user->currency).' ';
1279         $body .= ($canViewReports || $isClient) ? $totals['cost'] : $totals['expenses'];
1280         $body .= '</td>';
1281       }
1282       $body .= '</tr>';
1283
1284       $body .= '</table>';
1285     } else {
1286       // Regular report.
1287
1288       // Print table header.
1289       $body .= '<table border="0" cellpadding="4" cellspacing="0" width="100%">';
1290       $body .= '<tr>';
1291       $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.date').'</td>';
1292       if ($canViewReports || $isClient)
1293         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.user').'</td>';
1294       if ($bean->getAttribute('chclient'))
1295         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.client').'</td>';
1296       if ($bean->getAttribute('chproject'))
1297         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.project').'</td>';
1298       if ($bean->getAttribute('chtask'))
1299         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.task').'</td>';
1300       if ($bean->getAttribute('chcf_1'))
1301         $body .= '<td style="'.$tableHeader.'">'.htmlspecialchars($custom_fields->fields[0]['label']).'</td>';
1302       if ($bean->getAttribute('chstart'))
1303         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.start').'</td>';
1304       if ($bean->getAttribute('chfinish'))
1305         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.finish').'</td>';
1306       if ($bean->getAttribute('chduration'))
1307         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.duration').'</td>';
1308       if ($bean->getAttribute('chunits'))
1309         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.work_units_short').'</td>';
1310       if ($bean->getAttribute('chnote'))
1311         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.note').'</td>';
1312       if ($bean->getAttribute('chcost'))
1313         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.cost').'</td>';
1314       if ($bean->getAttribute('chpaid'))
1315         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.paid').'</td>';
1316       if ($bean->getAttribute('chip'))
1317         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.ip').'</td>';
1318       if ($bean->getAttribute('chinvoice'))
1319         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.invoice').'</td>';
1320       $body .= '</tr>';
1321
1322       // Initialize variables to print subtotals.
1323       if ($items && 'no_grouping' != $group_by) {
1324         $print_subtotals = true;
1325         $first_pass = true;
1326         $prev_grouped_by = '';
1327         $cur_grouped_by = '';
1328       }
1329       // Initialize variables to alternate color of rows for different dates.
1330       $prev_date = '';
1331       $cur_date = '';
1332       $row_style = $rowItem;
1333
1334       // Print report items.
1335       if (is_array($items)) {
1336         foreach ($items as $record) {
1337           $cur_date = $record['date'];
1338           // Print a subtotal row after a block of grouped items.
1339           if ($print_subtotals) {
1340             $cur_grouped_by = $record['grouped_by'];
1341             if ($cur_grouped_by != $prev_grouped_by && !$first_pass) {
1342               $body .= '<tr style="'.$rowSubtotal.'">';
1343               $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.subtotal').'</td>';
1344               $subtotal_name = htmlspecialchars($subtotals[$prev_grouped_by]['name']);
1345               if ($canViewReports || $isClient) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'user' ? $subtotal_name : '').'</td>';
1346               if ($bean->getAttribute('chclient')) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'client' ? $subtotal_name : '').'</td>';
1347               if ($bean->getAttribute('chproject')) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'project' ? $subtotal_name : '').'</td>';
1348               if ($bean->getAttribute('chtask')) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'task' ? $subtotal_name : '').'</td>';
1349               if ($bean->getAttribute('chcf_1')) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'cf_1' ? $subtotal_name : '').'</td>';
1350               if ($bean->getAttribute('chstart')) $body .= '<td></td>';
1351               if ($bean->getAttribute('chfinish')) $body .= '<td></td>';
1352               if ($bean->getAttribute('chduration')) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['time'].'</td>';
1353               if ($bean->getAttribute('chunits')) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['units'].'</td>';
1354               if ($bean->getAttribute('chnote')) $body .= '<td></td>';
1355               if ($bean->getAttribute('chcost')) {
1356                 $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1357                 $body .= ($canViewReports || $isClient) ? $subtotals[$prev_grouped_by]['cost'] : $subtotals[$prev_grouped_by]['expenses'];
1358                 $body .= '</td>';
1359               }
1360               if ($bean->getAttribute('chpaid')) $body .= '<td></td>';
1361               if ($bean->getAttribute('chip')) $body .= '<td></td>';
1362               if ($bean->getAttribute('chinvoice')) $body .= '<td></td>';
1363               $body .= '</tr>';
1364               $body .= '<tr><td>&nbsp;</td></tr>';
1365             }
1366             $first_pass = false;
1367           }
1368
1369           // Print a regular row.
1370           if ($cur_date != $prev_date)
1371             $row_style = ($row_style == $rowItem) ? $rowItemAlt : $rowItem;
1372           $body .= '<tr style="'.$row_style.'">';
1373           $body .= '<td style="'.$cellLeftAligned.'">'.$record['date'].'</td>';
1374           if ($canViewReports || $isClient)
1375             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['user']).'</td>';
1376           if ($bean->getAttribute('chclient'))
1377             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['client']).'</td>';
1378           if ($bean->getAttribute('chproject'))
1379             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['project']).'</td>';
1380           if ($bean->getAttribute('chtask'))
1381             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['task']).'</td>';
1382           if ($bean->getAttribute('chcf_1'))
1383             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['cf_1']).'</td>';
1384           if ($bean->getAttribute('chstart'))
1385             $body .= '<td nowrap style="'.$cellRightAligned.'">'.$record['start'].'</td>';
1386           if ($bean->getAttribute('chfinish'))
1387             $body .= '<td nowrap style="'.$cellRightAligned.'">'.$record['finish'].'</td>';
1388           if ($bean->getAttribute('chduration'))
1389             $body .= '<td style="'.$cellRightAligned.'">'.$record['duration'].'</td>';
1390           if ($bean->getAttribute('chunits'))
1391             $body .= '<td style="'.$cellRightAligned.'">'.$record['units'].'</td>';
1392           if ($bean->getAttribute('chnote'))
1393             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['note']).'</td>';
1394           if ($bean->getAttribute('chcost'))
1395             $body .= '<td style="'.$cellRightAligned.'">'.$record['cost'].'</td>';
1396           if ($bean->getAttribute('chpaid')) {
1397             $body .= '<td style="'.$cellRightAligned.'">';
1398             $body .= $record['paid'] == 1 ? $i18n->get('label.yes') : $i18n->get('label.no');
1399             $body .= '</td>';
1400           }
1401           if ($bean->getAttribute('chip')) {
1402             $body .= '<td style="'.$cellRightAligned.'">';
1403             $body .= $record['modified'] ? $record['modified_ip'].' '.$record['modified'] : $record['created_ip'].' '.$record['created'];
1404             $body .= '</td>';
1405           }
1406           if ($bean->getAttribute('chinvoice'))
1407             $body .= '<td style="'.$cellRightAligned.'">'.htmlspecialchars($record['invoice']).'</td>';
1408           $body .= '</tr>';
1409
1410           $prev_date = $record['date'];
1411           if ($print_subtotals)
1412             $prev_grouped_by = $record['grouped_by'];
1413         }
1414       }
1415
1416       // Print a terminating subtotal.
1417       if ($print_subtotals) {
1418         $body .= '<tr style="'.$rowSubtotal.'">';
1419         $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.subtotal').'</td>';
1420         $subtotal_name = htmlspecialchars($subtotals[$cur_grouped_by]['name']);
1421         if ($canViewReports || $isClient) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'user' ? $subtotal_name : '').'</td>';
1422         if ($bean->getAttribute('chclient')) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'client' ? $subtotal_name : '').'</td>';
1423         if ($bean->getAttribute('chproject')) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'project' ? $subtotal_name : '').'</td>';
1424         if ($bean->getAttribute('chtask')) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'task' ? $subtotal_name : '').'</td>';
1425         if ($bean->getAttribute('chcf_1')) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'cf_1' ? $subtotal_name : '').'</td>';
1426         if ($bean->getAttribute('chstart')) $body .= '<td></td>';
1427         if ($bean->getAttribute('chfinish')) $body .= '<td></td>';
1428         if ($bean->getAttribute('chduration')) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$cur_grouped_by]['time'].'</td>';
1429         if ($bean->getAttribute('chunits')) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$cur_grouped_by]['units'].'</td>';
1430         if ($bean->getAttribute('chnote')) $body .= '<td></td>';
1431         if ($bean->getAttribute('chcost')) {
1432           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1433           $body .= ($canViewReports || $isClient) ? $subtotals[$cur_grouped_by]['cost'] : $subtotals[$cur_grouped_by]['expenses'];
1434           $body .= '</td>';
1435         }
1436         if ($bean->getAttribute('chpaid')) $body .= '<td></td>';
1437         if ($bean->getAttribute('chip')) $body .= '<td></td>';
1438         if ($bean->getAttribute('chinvoice')) $body .= '<td></td>';
1439         $body .= '</tr>';
1440       }
1441
1442       // Print totals.
1443       $body .= '<tr><td>&nbsp;</td></tr>';
1444       $body .= '<tr style="'.$rowSubtotal.'">';
1445       $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.total').'</td>';
1446       if ($canViewReports || $isClient) $body .= '<td></td>';
1447       if ($bean->getAttribute('chclient')) $body .= '<td></td>';
1448       if ($bean->getAttribute('chproject')) $body .= '<td></td>';
1449       if ($bean->getAttribute('chtask')) $body .= '<td></td>';
1450       if ($bean->getAttribute('chcf_1')) $body .= '<td></td>';
1451       if ($bean->getAttribute('chstart')) $body .= '<td></td>';
1452       if ($bean->getAttribute('chfinish')) $body .= '<td></td>';
1453       if ($bean->getAttribute('chduration')) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$totals['time'].'</td>';
1454       if ($bean->getAttribute('chunits')) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$totals['units'].'</td>';
1455       if ($bean->getAttribute('chnote')) $body .= '<td></td>';
1456       if ($bean->getAttribute('chcost')) {
1457         $body .= '<td nowrap style="'.$cellRightAlignedSubtotal.'">'.htmlspecialchars($user->currency).' ';
1458         $body .= ($canViewReports || $isClient) ? $totals['cost'] : $totals['expenses'];
1459         $body .= '</td>';
1460       }
1461       if ($bean->getAttribute('chpaid')) $body .= '<td></td>';
1462       if ($bean->getAttribute('chip')) $body .= '<td></td>';
1463       if ($bean->getAttribute('chinvoice')) $body .= '<td></td>';
1464       $body .= '</tr>';
1465
1466       $body .= '</table>';
1467     }
1468
1469     // Output footer.
1470     if (!defined('REPORT_FOOTER') || !(REPORT_FOOTER == false))
1471       $body .= '<p style="text-align: center;">'.$i18n->get('form.mail.footer').'</p>';
1472
1473     // Finish creating email body.
1474     $body .= '</body></html>';
1475
1476     return $body;
1477   }
1478
1479   // checkFavReportCondition - checks whether it is okay to send fav report.
1480   static function checkFavReportCondition($report, $condition)
1481   {
1482     $items = ttReportHelper::getFavItems($report);
1483
1484     $condition = str_replace('count', '', $condition);
1485     $count_required = (int) trim(str_replace('>', '', $condition));
1486
1487     if (count($items) > $count_required)
1488       return true; // Condition ok.
1489
1490     return false;
1491   }
1492
1493   // prepareFavReportBody - prepares an email body for a favorite report.
1494   static function prepareFavReportBody($report)
1495   {
1496     global $user;
1497     global $i18n;
1498
1499     // Determine these once as they are used in multiple places in this function.
1500     $canViewReports = $user->can('view_reports');
1501     $isClient = $user->isClient();
1502
1503     $items = ttReportHelper::getFavItems($report);
1504     $group_by = $report['group_by'];
1505     if ($group_by && 'no_grouping' != $group_by)
1506       $subtotals = ttReportHelper::getFavSubtotals($report);
1507     $totals = ttReportHelper::getFavTotals($report);
1508
1509     // Use custom fields plugin if it is enabled.
1510     if ($user->isPluginEnabled('cf'))
1511       $custom_fields = new CustomFields($user->group_id);
1512
1513     // Define some styles to use in email.
1514     $style_title = 'text-align: center; font-size: 15pt; font-family: Arial, Helvetica, sans-serif;';
1515     $tableHeader = 'font-weight: bold; background-color: #a6ccf7; text-align: left;';
1516     $tableHeaderCentered = 'font-weight: bold; background-color: #a6ccf7; text-align: center;';
1517     $rowItem = 'background-color: #ffffff;';
1518     $rowItemAlt = 'background-color: #f5f5f5;';
1519     $rowSubtotal = 'background-color: #e0e0e0;';
1520     $cellLeftAligned = 'text-align: left; vertical-align: top;';
1521     $cellRightAligned = 'text-align: right; vertical-align: top;';
1522     $cellLeftAlignedSubtotal = 'font-weight: bold; text-align: left; vertical-align: top;';
1523     $cellRightAlignedSubtotal = 'font-weight: bold; text-align: right; vertical-align: top;';
1524
1525     // Start creating email body.
1526     $body = '<html>';
1527     $body .= '<head><meta http-equiv="content-type" content="text/html; charset='.CHARSET.'"></head>';
1528     $body .= '<body>';
1529
1530     // Output title.
1531     $body .= '<p style="'.$style_title.'">'.$i18n->get('form.mail.report_subject').': '.$totals['start_date'].' - '.$totals['end_date'].'</p>';
1532
1533     // Output comment.
1534     // if ($comment) $body .= '<p>'.htmlspecialchars($comment).'</p>'; // No comment for fav. reports.
1535
1536     if ($report['show_totals_only']) {
1537       // Totals only report. Output subtotals.
1538
1539       // Determine group_by header.
1540       if ('cf_1' == $group_by)
1541         $group_by_header = htmlspecialchars($custom_fields->fields[0]['label']);
1542       else {
1543         $key = 'label.'.$group_by;
1544         $group_by_header = $i18n->get($key);
1545       }
1546
1547       $body .= '<table border="0" cellpadding="4" cellspacing="0" width="100%">';
1548       $body .= '<tr>';
1549       $body .= '<td style="'.$tableHeader.'">'.$group_by_header.'</td>';
1550       if ($report['show_duration'])
1551         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.duration').'</td>';
1552       if ($report['show_cost'])
1553         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.cost').'</td>';
1554       $body .= '</tr>';
1555       foreach($subtotals as $subtotal) {
1556         $body .= '<tr style="'.$rowSubtotal.'">';
1557         $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($subtotal['name'] ? htmlspecialchars($subtotal['name']) : '&nbsp;').'</td>';
1558         if ($report['show_duration']) {
1559           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1560           if ($subtotal['time'] <> '0:00') $body .= $subtotal['time'];
1561           $body .= '</td>';
1562         }
1563         if ($report['show_cost']) {
1564           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1565           $body .= ($canViewReports || $isClient) ? $subtotal['cost'] : $subtotal['expenses'];
1566           $body .= '</td>';
1567         }
1568         $body .= '</tr>';
1569       }
1570
1571       // Print totals.
1572       $body .= '<tr><td>&nbsp;</td></tr>';
1573       $body .= '<tr style="'.$rowSubtotal.'">';
1574       $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.total').'</td>';
1575       if ($report['show_duration']) {
1576         $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1577         if ($totals['time'] <> '0:00') $body .= $totals['time'];
1578         $body .= '</td>';
1579       }
1580       if ($report['show_cost']) {
1581         $body .= '<td nowrap style="'.$cellRightAlignedSubtotal.'">'.htmlspecialchars($user->currency).' ';
1582         $body .= ($canViewReports || $isClient) ? $totals['cost'] : $totals['expenses'];
1583         $body .= '</td>';
1584       }
1585       $body .= '</tr>';
1586
1587       $body .= '</table>';
1588     } else {
1589       // Regular report.
1590
1591       // Print table header.
1592       $body .= '<table border="0" cellpadding="4" cellspacing="0" width="100%">';
1593       $body .= '<tr>';
1594       $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.date').'</td>';
1595       if ($canViewReports || $isClient)
1596         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.user').'</td>';
1597       if ($report['show_client'])
1598         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.client').'</td>';
1599       if ($report['show_project'])
1600         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.project').'</td>';
1601       if ($report['show_task'])
1602         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.task').'</td>';
1603       if ($report['show_custom_field_1'])
1604         $body .= '<td style="'.$tableHeader.'">'.htmlspecialchars($custom_fields->fields[0]['label']).'</td>';
1605       if ($report['show_start'])
1606         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.start').'</td>';
1607       if ($report['show_end'])
1608         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.finish').'</td>';
1609       if ($report['show_duration'])
1610         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.duration').'</td>';
1611       if ($report['show_note'])
1612         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.note').'</td>';
1613       if ($report['show_cost'])
1614         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.cost').'</td>';
1615       if ($report['show_paid'])
1616         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.paid').'</td>';
1617       if ($report['show_ip'])
1618         $body .= '<td style="'.$tableHeaderCentered.'" width="5%">'.$i18n->get('label.ip').'</td>';
1619       if ($report['show_invoice'])
1620         $body .= '<td style="'.$tableHeader.'">'.$i18n->get('label.invoice').'</td>';
1621       $body .= '</tr>';
1622
1623       // Initialize variables to print subtotals.
1624       if ($items && 'no_grouping' != $group_by) {
1625         $print_subtotals = true;
1626         $first_pass = true;
1627         $prev_grouped_by = '';
1628         $cur_grouped_by = '';
1629       }
1630       // Initialize variables to alternate color of rows for different dates.
1631       $prev_date = '';
1632       $cur_date = '';
1633       $row_style = $rowItem;
1634
1635       // Print report items.
1636       if (is_array($items)) {
1637         foreach ($items as $record) {
1638           $cur_date = $record['date'];
1639           // Print a subtotal row after a block of grouped items.
1640           if ($print_subtotals) {
1641             $cur_grouped_by = $record['grouped_by'];
1642             if ($cur_grouped_by != $prev_grouped_by && !$first_pass) {
1643               $body .= '<tr style="'.$rowSubtotal.'">';
1644               $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.subtotal').'</td>';
1645               $subtotal_name = htmlspecialchars($subtotals[$prev_grouped_by]['name']);
1646               if ($canViewReports || $isClient) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'user' ? $subtotal_name : '').'</td>';
1647               if ($report['show_client']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'client' ? $subtotal_name : '').'</td>';
1648               if ($report['show_project']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'project' ? $subtotal_name : '').'</td>';
1649               if ($report['show_task']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'task' ? $subtotal_name : '').'</td>';
1650               if ($report['show_custom_field_1']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'cf_1' ? $subtotal_name : '').'</td>';
1651               if ($report['show_start']) $body .= '<td></td>';
1652               if ($report['show_end']) $body .= '<td></td>';
1653               if ($report['show_duration']) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$subtotals[$prev_grouped_by]['time'].'</td>';
1654               if ($report['show_note']) $body .= '<td></td>';
1655               if ($report['show_cost']) {
1656                 $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1657                 $body .= ($canViewReports || $isClient) ? $subtotals[$prev_grouped_by]['cost'] : $subtotals[$prev_grouped_by]['expenses'];
1658                 $body .= '</td>';
1659               }
1660               if ($report['show_paid']) $body .= '<td></td>';
1661               if ($report['show_ip']) $body .= '<td></td>';
1662               if ($report['show_invoice']) $body .= '<td></td>';
1663               $body .= '</tr>';
1664               $body .= '<tr><td>&nbsp;</td></tr>';
1665             }
1666             $first_pass = false;
1667           }
1668
1669           // Print a regular row.
1670           if ($cur_date != $prev_date)
1671             $row_style = ($row_style == $rowItem) ? $rowItemAlt : $rowItem;
1672           $body .= '<tr style="'.$row_style.'">';
1673           $body .= '<td style="'.$cellLeftAligned.'">'.$record['date'].'</td>';
1674           if ($canViewReports || $isClient)
1675             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['user']).'</td>';
1676           if ($report['show_client'])
1677             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['client']).'</td>';
1678           if ($report['show_project'])
1679             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['project']).'</td>';
1680           if ($report['show_task'])
1681             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['task']).'</td>';
1682           if ($report['show_custom_field_1'])
1683             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['cf_1']).'</td>';
1684           if ($report['show_start'])
1685             $body .= '<td nowrap style="'.$cellRightAligned.'">'.$record['start'].'</td>';
1686           if ($report['show_end'])
1687             $body .= '<td nowrap style="'.$cellRightAligned.'">'.$record['finish'].'</td>';
1688           if ($report['show_duration'])
1689             $body .= '<td style="'.$cellRightAligned.'">'.$record['duration'].'</td>';
1690           if ($report['show_note'])
1691             $body .= '<td style="'.$cellLeftAligned.'">'.htmlspecialchars($record['note']).'</td>';
1692           if ($report['show_cost'])
1693             $body .= '<td style="'.$cellRightAligned.'">'.$record['cost'].'</td>';
1694           if ($report['show_paid']) {
1695             $body .= '<td style="'.$cellRightAligned.'">';
1696             $body .= $record['paid'] == 1 ? $i18n->get('label.yes') : $i18n->get('label.no');
1697             $body .= '</td>';
1698           }
1699           if ($report['show_ip']) {
1700             $body .= '<td style="'.$cellRightAligned.'">';
1701             $body .= $record['modified'] ? $record['modified_ip'].' '.$record['modified'] : $record['created_ip'].' '.$record['created'];
1702             $body .= '</td>';
1703           }
1704           if ($report['show_invoice'])
1705             $body .= '<td style="'.$cellRightAligned.'">'.htmlspecialchars($record['invoice']).'</td>';
1706           $body .= '</tr>';
1707
1708           $prev_date = $record['date'];
1709           if ($print_subtotals)
1710             $prev_grouped_by = $record['grouped_by'];
1711         }
1712       }
1713
1714       // Print a terminating subtotal.
1715       if ($print_subtotals) {
1716         $body .= '<tr style="'.$rowSubtotal.'">';
1717         $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.subtotal').'</td>';
1718         $subtotal_name = htmlspecialchars($subtotals[$cur_grouped_by]['name']);
1719         if ($canViewReports || $isClient) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'user' ? $subtotal_name : '').'</td>';
1720         if ($report['show_client']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'client' ? $subtotal_name : '').'</td>';
1721         if ($report['show_project']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'project' ? $subtotal_name : '').'</td>';
1722         if ($report['show_task']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'task' ? $subtotal_name : '').'</td>';
1723         if ($report['show_custom_field_1']) $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.($group_by == 'cf_1' ? $subtotal_name : '').'</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.'">'.$subtotals[$cur_grouped_by]['time'].'</td>';
1727         if ($report['show_note']) $body .= '<td></td>';
1728         if ($report['show_cost']) {
1729           $body .= '<td style="'.$cellRightAlignedSubtotal.'">';
1730           $body .= ($canViewReports || $isClient) ? $subtotals[$cur_grouped_by]['cost'] : $subtotals[$cur_grouped_by]['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
1739       // Print totals.
1740       $body .= '<tr><td>&nbsp;</td></tr>';
1741       $body .= '<tr style="'.$rowSubtotal.'">';
1742       $body .= '<td style="'.$cellLeftAlignedSubtotal.'">'.$i18n->get('label.total').'</td>';
1743       if ($canViewReports || $isClient) $body .= '<td></td>';
1744       if ($report['show_client']) $body .= '<td></td>';
1745       if ($report['show_project']) $body .= '<td></td>';
1746       if ($report['show_task']) $body .= '<td></td>';
1747       if ($report['show_custom_field_1']) $body .= '<td></td>';
1748       if ($report['show_start']) $body .= '<td></td>';
1749       if ($report['show_end']) $body .= '<td></td>';
1750       if ($report['show_duration']) $body .= '<td style="'.$cellRightAlignedSubtotal.'">'.$totals['time'].'</td>';
1751       if ($report['show_note']) $body .= '<td></td>';
1752       if ($report['show_cost']) {
1753         $body .= '<td nowrap style="'.$cellRightAlignedSubtotal.'">'.htmlspecialchars($user->currency).' ';
1754         $body .= ($canViewReports || $isClient) ? $totals['cost'] : $totals['expenses'];
1755         $body .= '</td>';
1756       }
1757       if ($report['show_paid']) $body .= '<td></td>';
1758       if ($report['show_ip']) $body .= '<td></td>';
1759       if ($report['show_invoice']) $body .= '<td></td>';
1760       $body .= '</tr>';
1761
1762       $body .= '</table>';
1763     }
1764
1765     // Output footer.
1766     if (!defined('REPORT_FOOTER') || !(REPORT_FOOTER == false))
1767       $body .= '<p style="text-align: center;">'.$i18n->get('form.mail.footer').'</p>';
1768
1769     // Finish creating email body.
1770     $body .= '</body></html>';
1771
1772     return $body;
1773   }
1774
1775   // sendFavReport - sends a favorite report to a specified email, called from cron.php
1776   static function sendFavReport($report, $subject, $email, $cc) {
1777     // We are called from cron.php, we have no $bean in session.
1778     // cron.php sets global $user and $i18n objects to match our favorite report user.
1779     global $user;
1780     global $i18n;
1781
1782     // Prepare report body.
1783     $body = ttReportHelper::prepareFavReportBody($report);
1784
1785     import('mail.Mailer');
1786     $mailer = new Mailer();
1787     $mailer->setCharSet(CHARSET);
1788     $mailer->setContentType('text/html');
1789     $mailer->setSender(SENDER);
1790     if (!empty($cc))
1791       $mailer->setReceiverCC($cc);
1792     if (!empty($user->bcc_email))
1793       $mailer->setReceiverBCC($user->bcc_email);
1794     $mailer->setReceiver($email);
1795     $mailer->setMailMode(MAIL_MODE);
1796     if (empty($subject)) $subject = $report['name'];
1797     if (!$mailer->send($subject, $body))
1798       return false;
1799
1800     return true;
1801   }
1802 }