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