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