Issues (4868)

timesheet/inc/class.timesheet_bo.inc.php (27 issues)

1
<?php
2
/**
3
 * TimeSheet - business object
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7
 * @package timesheet
8
 * @copyright (c) 2005-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 * @version $Id$
11
 */
12
13
use EGroupware\Api;
14
use EGroupware\Api\Link;
15
use EGroupware\Api\Acl;
16
17
if (!defined('TIMESHEET_APP'))
18
{
19
	define('TIMESHEET_APP','timesheet');
20
}
21
22
23
/**
24
 * Business object of the TimeSheet
25
 *
26
 * Uses eTemplate's Api\Storage as storage object (Table: egw_timesheet).
27
 */
28
class timesheet_bo extends Api\Storage
29
{
30
	/**
31
	 * Flag for timesheets deleted, but preserved
32
	 */
33
	const DELETED_STATUS = -1;
34
35
	/**
36
	 * Timesheets Api\Config data
37
	 *
38
	 * @var array
39
	 */
40
	var $config_data = array();
41
	/**
42
	 * Should we show a quantity sum, makes only sense if we sum up identical units (can be used to sum up negative (over-)time)
43
	 *
44
	 * @var boolean
45
	 */
46
	var $quantity_sum=false;
47
	/**
48
	 * current user
49
	 *
50
	 * @var int
51
	 */
52
	var $user;
53
	/**
54
	 * Timestaps that need to be adjusted to user-time on reading or saving
55
	 *
56
	 * @var array
57
	 */
58
	var $timestamps = array(
59
		'ts_start','ts_created', 'ts_modified'
60
	);
61
	/**
62
	 * Start of today in user-time
63
	 *
64
	 * @var int
65
	 */
66
	var $today;
67
	/**
68
	 * Filter for search limiting the date-range
69
	 *
70
	 * @var array
71
	 */
72
	var $date_filters = array(	// Start: year,month,day,week, End: year,month,day,week
73
		'Today'       => array(0,0,0,0,  0,0,1,0),
74
		'Yesterday'   => array(0,0,-1,0, 0,0,0,0),
75
		'This week'   => array(0,0,0,0,  0,0,0,1),
76
		'Last week'   => array(0,0,0,-1, 0,0,0,0),
77
		'This month'  => array(0,0,0,0,  0,1,0,0),
78
		'Last month'  => array(0,-1,0,0, 0,0,0,0),
79
		'2 month ago' => array(0,-2,0,0, 0,-1,0,0),
80
		'This year'   => array(0,0,0,0,  1,0,0,0),
81
		'Last year'   => array(-1,0,0,0, 0,0,0,0),
82
		'2 years ago' => array(-2,0,0,0, -1,0,0,0),
83
		'3 years ago' => array(-3,0,0,0, -2,0,0,0),
84
	);
85
	/**
86
	 * Grants: $GLOBALS['egw']->acl->get_grants(TIMESHEET_APP);
87
	 *
88
	 * @var array
89
	 */
90
	var $grants;
91
	/**
92
	 * Sums of the last search in keys duration and price
93
	 *
94
	 * @var array
95
	 */
96
	var $summary;
97
	/**
98
	 * Array with boolean values in keys 'day', 'week' or 'month', for the sums to return in the search
99
	 *
100
	 * @var array
101
	 */
102
	var $show_sums;
103
	/**
104
	 * Array with custom fileds
105
	 *
106
	 * @var array
107
	 */
108
	var $customfields=array();
109
	/**
110
	 * Array with status label
111
	 *
112
	 * @var array
113
	 */
114
	var $status_labels = array();
115
	/**
116
	 * Array with status label configuration
117
	 *
118
	 * @var array
119
	 */
120
	var $status_labels_config = array();
121
	/**
122
	 * Instance of the timesheet_tracking object
123
	 *
124
	 * @var timesheet_tracking
125
	 */
126
	var $tracking;
127
	/**
128
	 * Translates field / acl-names to labels
129
	 *
130
	 * @var array
131
	 */
132
	var $field2label = array(
133
		'ts_project'     => 'Project',
134
		'ts_title'     	 => 'Title',
135
		'cat_id'         => 'Category',
136
		'ts_description' => 'Description',
137
		'ts_start'       => 'Start',
138
		'ts_duration'    => 'Duration',
139
		'ts_quantity'    => 'Quantity',
140
		'ts_unitprice'   => 'Unitprice',
141
		'ts_owner'       => 'Owner',
142
		'ts_modifier'    => 'Modifier',
143
		'ts_status'      => 'Status',
144
		'pm_id'		     => 'Projectid',
145
		// pseudo fields used in edit
146
		//'link_to'        => 'Attachments & Links',
147
		'customfields'   => 'Custom fields',
148
	);
149
	/**
150
	 * Name of the timesheet table storing custom fields
151
	 */
152
	const EXTRA_TABLE = 'egw_timesheet_extra';
153
154
	/**
155
	* Columns to search when user does a text search
156
	*/
157
	var $columns_to_search = array('egw_timesheet.ts_id', 'ts_project', 'ts_title', 'ts_description', 'ts_duration', 'ts_quantity', 'ts_unitprice');
158
159
	/**
160
	 * all cols in data which are not (direct)in the db, for data_merge
161
	 *
162
	 * @var array
163
	 */
164
	var $non_db_cols = array('pm_id');
165
166
	function __construct()
167
	{
168
		parent::__construct(TIMESHEET_APP,'egw_timesheet',self::EXTRA_TABLE,'','ts_extra_name','ts_extra_value','ts_id');
169
170
		$this->config_data = Api\Config::read(TIMESHEET_APP);
171
		$this->quantity_sum = $this->config_data['quantity_sum'] == 'true';
172
173
		// Load & process statuses
174
		if($this->config_data['status_labels']) $this->load_statuses();
175
176
		$this->today = mktime(0,0,0,date('m',$this->now),date('d',$this->now),date('Y',$this->now));
177
178
		// save us in $GLOBALS['timesheet_bo'] for ExecMethod used in hooks
179
		if (!is_object($GLOBALS['timesheet_bo']))
180
		{
181
			$GLOBALS['timesheet_bo'] =& $this;
182
		}
183
		$this->grants = $GLOBALS['egw']->acl->get_grants(TIMESHEET_APP);
184
	}
185
186
	/**
187
	 * Load status labels
188
	 */
189
	protected function load_statuses()
190
	{
191
		$this->status_labels =&  $this->config_data['status_labels'];
192
		if (!is_array($this->status_labels)) $this->status_labels= array($this->status_labels);
193
194
		foreach ($this->status_labels as $status_id => $label)
195
		{
196
			if (!is_array($label))
197
			{	//old values, before parent status
198
				$name = $label;
199
				$label=array();
200
				$label['name'] = $name;
201
				$label['parent'] = '';
202
			}
203
			$label['id'] = $status_id;
204
			$this->status_labels_config[$status_id] = $label;
205
		}
206
207
		// Organise into tree structure
208
		$map = array(
209
			'' => array('substatus' => array())
210
		);
211
		foreach($this->status_labels_config as $id => &$status)
212
		{
213
			$status['substatus'] = array();
214
			$map[$id] = &$status;
215
		}
216
		foreach($this->status_labels_config as &$status)
217
		{
218
			$map[$status['parent']]['substatus'][] = &$status;
219
		}
220
		$tree = $map['']['substatus'];
221
222
		// Make nice selectbox labels
223
		$this->status_labels = array();
224
		$this->make_status_labels($tree, $this->status_labels);
225
226
		// Sort Api\Config based on tree
227
		$sorted = array();
228
		foreach($this->status_labels as $status_id => $label)
229
		{
230
			$sorted[$status_id] = $this->status_labels_config[$status_id];
231
			//$sorted[$status_id]['name'] = $label;
232
			unset($sorted[$status_id]['substatus']);
233
		}
234
		$this->status_labels_config = $sorted;
235
	}
236
237
	/**
238
	 * Return evtl. existing sub-statuses of given status
239
	 *
240
	 * @param int $status
241
	 * @return array|int with sub-statuses incl. $status or just $status
242
	 */
243
	function get_sub_status($status)
244
	{
245
		if (!isset($this->status_labels_config)) $this->load_statuses();
246
		$stati = array($status);
247
		foreach($this->status_labels_config as $stat)
248
		{
249
			if ($stat['parent'] && in_array($stat['parent'], $stati))
250
			{
251
				$stati[] = $stat['id'];
252
			}
253
		}
254
		//error_log(__METHOD__."($status) returning ".array2string(count($stati) == 1 ? $status : $stati));
255
		return count($stati) == 1 ? $status : $stati;
256
	}
257
258
	/**
259
	 * Make nice labels with leading spaces depending on depth
260
	 *
261
	 * @param statuses List of statuses to process, with sub-statuses in a 'substatus' array
0 ignored issues
show
The type List was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
262
	 * @param labels Array of labels, pass array() and labels will be built in it
263
	 * @param depth Current depth
0 ignored issues
show
The type Current was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
264
	 *
265
	 * @return None, labels are built in labels parameter
0 ignored issues
show
The type None was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
266
	 */
267
	protected function make_status_labels($statuses, &$labels, $depth=0)
268
	{
269
		foreach($statuses as $status)
270
		{
271
			$labels[$status['id']] = str_pad('',$depth*12, "&nbsp;",STR_PAD_LEFT).trim(str_replace('&nbsp;','',$status['name']));
272
			if(count($status['substatus']) > 0)
273
			{
274
				$this->make_status_labels($status['substatus'], $labels, $depth+1);
275
			}
276
		}
277
	}
278
279
	/**
280
	 * Get status labels with admin statuses (optionally) filtered out
281
	 *
282
	 * @param boolean $admin
283
	 *
284
	 * @return Array
285
	 */
286
	protected function get_status_labels($admin = null)
287
	{
288
		if(is_null($admin))
289
		{
290
			$admin = isset($GLOBALS['egw_info']['user']['apps']['admin']);
291
		}
292
		$labels = array();
293
		foreach($this->status_labels as $status_id => $label)
294
		{
295
			if($admin || !$admin && !$this->status_labels_config[$status_id]['admin'])
296
			{
297
				$labels[$status_id] = $label;
298
			}
299
		}
300
		return $labels;
301
	}
302
303
	/**
304
	 * get list of specified grants as uid => Username pairs
305
	 *
306
	 * @param int $required =Acl::READ
307
	 * @param boolean $hide_deactive =null default only Acl::EDIT hides deactivates users
308
	 * @return array with uid => Username pairs
309
	 */
310
	function grant_list($required=Acl::READ, $hide_deactive=null)
311
	{
312
		if (!isset($hide_deactive)) $hide_deactive = $required == Acl::EDIT;
313
314
		$result = array();
315
		foreach($this->grants as $uid => $grant)
316
		{
317
			if ($grant & $required && (!$hide_deactive || Api\Accounts::getInstance()->is_active($uid)))
318
			{
319
				$result[$uid] = Api\Accounts::username($uid);
320
			}
321
		}
322
		natcasesort($result);
323
324
		return $result;
325
	}
326
327
	/**
328
	 * checks if the user has enough rights for a certain operation
329
	 *
330
	 * Rights are given via status Api\Config admin/noadmin
331
	 *
332
	 * @param array|int $data =null use $this->data or $this->data['ts_id'] (to fetch the data)
333
	 * @param int $user =null for which user to check, default current user
334
	 * @return boolean true if the rights are ok, false if no rights
335
	 */
336
	function check_statusForEditRights($data=null,$user=null)
337
	{
338
		if (is_null($data) || (int)$data == $this->data['ts_id'])
339
		{
340
			$data =& $this->data;
341
		}
342
		if (!is_array($data))
343
		{
344
			$save_data = $this->data;
345
			$data = $this->read($data,true);
346
			$this->data = $save_data;
347
348
			if (!$data) return null; 	// entry not found
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
$data is a non-empty array, thus ! $data is always false.
Loading history...
349
		}
350
		if (!$user) $user = $this->user;
0 ignored issues
show
The assignment to $user is dead and can be removed.
Loading history...
Bug Best Practice introduced by
The expression $user of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
351
		if (!isset($GLOBALS['egw_info']['user']['apps']['admin']) && $data['ts_status'])
352
		{
353
			if ($this->status_labels_config[$data['ts_status']]['admin'])
354
			{
355
				return false;
356
			}
357
		}
358
		return true;
359
	}
360
361
	/**
362
	 * checks if the user has enough rights for a certain operation
363
	 *
364
	 * Rights are given via owner grants or role based Acl
365
	 *
366
	 * @param int $required Acl::READ, EGW_ACL_WRITE, Acl::ADD, Acl::DELETE, EGW_ACL_BUDGET, EGW_ACL_EDIT_BUDGET
367
	 * @param array|int $data =null project or project-id to use, default the project in $this->data
368
	 * @param int $user =null for which user to check, default current user
369
	 * @return boolean true if the rights are ok, null if not found, false if no rights
370
	 */
371
	function check_acl($required,$data=null,$user=null)
372
	{
373
		if (is_null($data) || (int)$data == $this->data['ts_id'])
374
		{
375
			$data =& $this->data;
376
		}
377
		if (!is_array($data))
378
		{
379
			$save_data = $this->data;
380
			$data = $this->read($data,true);
381
			$this->data = $save_data;
382
383
			if (!$data) return null; 	// entry not found
0 ignored issues
show
$data is a non-empty array, thus ! $data is always false.
Loading history...
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
384
		}
385
		if (!$user) $user = $this->user;
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
386
		if ($user == $this->user)
387
		{
388
			$grants = $this->grants;
389
		}
390
		else
391
		{
392
			$grants = $GLOBALS['egw']->acl->get_grants(TIMESHEET_APP,true,$user);
393
		}
394
		$ret = $data && !!($grants[$data['ts_owner']] & $required);
395
396
		if(($required & Acl::DELETE) && $this->config_data['history'] == 'history' &&
397
			$data['ts_status'] == self::DELETED_STATUS)
398
		{
399
			$ret = !!($GLOBALS['egw_info']['user']['apps']['admin']);
400
		}
401
		//error_log(__METHOD__."($required,$data[ts_id],$user) returning ".array2string($ret));
402
		return $ret;
403
	}
404
405
	/**
406
	 * return SQL implementing filtering by date
407
	 *
408
	 * @param string $name
409
	 * @param int &$start
410
	 * @param int &$end
411
	 * @return string
412
	 */
413
	function date_filter($name,&$start,&$end)
414
	{
415
		return Api\DateTime::sql_filter($name, $start, $end, 'ts_start', $this->date_filters);
416
	}
417
418
	/**
419
	 * search the timesheet
420
	 *
421
	 * reimplemented to limit result to users we have grants from
422
	 * Use $filter['ts_owner'] === false for no ACL check.
423
	 *
424
	 * @param array|string $criteria array of key and data cols, OR a SQL query (content for WHERE), fully quoted (!)
425
	 * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return
426
	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
427
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
428
	 * @param string $wildcard ='' appended befor and after each criteria
429
	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
430
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
431
	 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num)
432
	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
433
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
434
	 *	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
435
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
436
	 * @param boolean $only_summary =false If true only return the sums as array with keys duration and price, default false
437
	 * @return array of matching rows (the row is an array of the cols) or False
438
	 */
439
	function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='',$need_full_no_count=false,$only_summary=false)
440
	{
441
		//error_log(__METHOD__."(".print_r($criteria,true).",'$only_keys','$order_by',".print_r($extra_cols,true).",'$wildcard','$empty','$op','$start',".print_r($filter,true).",'$join')");
442
		//echo "<p>".__METHOD__."(".print_r($criteria,true).",'$only_keys','$order_by',".print_r($extra_cols,true).",'$wildcard','$empty','$op','$start',".print_r($filter,true).",'$join')</p>\n";
443
		// postgres can't round from double precission, only from numeric ;-)
444
		$total_sql = $this->db->Type != 'pgsql' ? "round(ts_quantity*ts_unitprice,2)" : "round(cast(ts_quantity*ts_unitprice AS numeric),2)";
445
446
		if (!is_array($extra_cols))
447
		{
448
			$extra_cols = $extra_cols ? explode(',',$extra_cols) : array();
449
		}
450
		if ($only_keys === false || $this->show_sums && strpos($order_by,'ts_start') !== false)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->show_sums of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
451
		{
452
			$extra_cols[] = $total_sql.' AS ts_total';
453
		}
454
		if (!isset($filter['ts_owner']) || !count($filter['ts_owner']))
455
		{
456
			$filter['ts_owner'] = array_keys($this->grants);
457
		}
458
		// $filter['ts_owner'] === false --> no ACL checks
459
		elseif ($filter['ts_owner'] === false)
460
		{
461
			$filter['ts_owner'] = '';
462
		}
463
		else
464
		{
465
			if (!is_array($filter['ts_owner'])) $filter['ts_owner'] = array($filter['ts_owner']);
466
467
			foreach($filter['ts_owner'] as $key => $owner)
468
			{
469
				if (!isset($this->grants[$owner]))
470
				{
471
					unset($filter['ts_owner'][$key]);
472
				}
473
			}
474
		}
475
		if (isset($filter['ts_status']) && $filter['ts_status'] && $filter['ts_status'] != self::DELETED_STATUS)
476
		{
477
			$filter['ts_status'] = $this->get_sub_status($filter['ts_status']);
478
		}
479
		else
480
		{
481
			$filter[] = '(ts_status ' . ($filter['ts_status'] == self::DELETED_STATUS ? '=':'!= ') . self::DELETED_STATUS .
482
				($filter['ts_status'] == self::DELETED_STATUS ? '':' OR ts_status IS NULL') . ')';
483
		}
484
		if (!count($filter['ts_owner']))
485
		{
486
			$this->total = 0;
487
			$this->summary = array();
488
			return array();
489
		}
490
		if ($only_summary==false && $criteria && $this->show_sums)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->show_sums of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
491
		{
492
			// if we have a criteria AND intend to show the sums we first query the affected ids,
493
			// then we throw away criteria and filter, and replace the filter with the list of ids
494
			$ids = parent::search($criteria,'egw_timesheet.ts_id as id','','',$wildcard,$empty,$op,false,$filter,$join);
495
			//_debug_array($ids);
496
			if (empty($ids))
497
			{
498
				$this->summary = array('duration'=>0,'price'=>null,'quantity'=>0);
499
				return array();
500
			}
501
			unset($criteria);
502
			foreach ($ids as $v)
503
			{
504
				$id_filter[] = $v['id'];
505
			}
506
			$filter = array('ts_id'=>$id_filter);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $id_filter seems to be defined by a foreach iteration on line 502. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
507
		}
508
		// if we only want to return the summary (sum of duration and sum of price) we have to take care that the customfield table
509
		// is not joined, as the join causes a multiplication of the sum per customfield found
510
		// joining of the cutomfield table is triggered by criteria being set with either a string or an array
511
		$this->summary = parent::search($only_summary ? null : $criteria,
512
			"SUM(ts_duration) AS duration,SUM($total_sql) AS price,MAX(ts_modified) AS max_modified".
513
				($this->quantity_sum ? ",SUM(ts_quantity) AS quantity" : ''),
514
			'', '', $wildcard, $empty, $op, false,
515
			$only_summary && is_array($criteria) ? ($filter ? array_merge($criteria, (array)$filter) : $criteria) : $filter,
516
			$only_summary ? '' : $join);
517
		$this->summary = $this->summary[0];
518
		$this->summary['max_modified'] = Api\DateTime::server2user($this->summary['max_modified']);
519
520
		if ($only_summary) return $this->summary;
521
522
		if ($this->show_sums && strpos($order_by,'ts_start') !== false && 	// sums only make sense if ordered by ts_start
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->show_sums of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
523
			$this->db->capabilities['union'] && ($from_unixtime_ts_start = $this->db->from_unixtime('ts_start')))
524
		{
525
			$sum_sql = array(
526
				'year'  => $this->db->date_format($from_unixtime_ts_start,'%Y'),
527
				'month' => $this->db->date_format($from_unixtime_ts_start,'%Y%m'),
528
				'week'  => $this->db->date_format($from_unixtime_ts_start,$GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'] == 'Sunday' ? '%X%V' : '%x%v'),
529
				'day'   => $this->db->date_format($from_unixtime_ts_start,'%Y-%m-%d'),
530
			);
531
			foreach($this->show_sums as $type)
532
			{
533
				$extra_cols[] = $sum_sql[$type].' AS ts_'.$type;
534
				$extra_cols[] = '0 AS is_sum_'.$type;
535
				$sum_extra_cols[] = str_replace('ts_start','MIN(ts_start)',$sum_sql[$type]);	// as we dont group by ts_start
536
				$sum_extra_cols[$type] = '0 AS is_sum_'.$type;
537
			}
538
			// regular entries
539
			parent::search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,'UNION',$filter,$join,$need_full_no_count);
540
541
			$sort = substr($order_by,8);
542
			$union_order = array();
543
			$sum_ts_id = array('year' => -3,'month' => -2,'week' => -1,'day' => 0);
544
			foreach($this->show_sums as $type)
545
			{
546
				$union_order[] = 'ts_'.$type . ' ' . $sort;
547
				$union_order[] = 'is_sum_'.$type;
548
				$sum_extra_cols[$type]{0} = '1';
549
				// the $type sum
550
				parent::search($criteria,array(
551
					(string)$sum_ts_id[$type],"''","''","''",'MIN(ts_start)','SUM(ts_duration) AS ts_duration',
552
					($this->quantity_sum ? "SUM(ts_quantity) AS ts_quantity" : '0'),
553
					'0','NULL','0','0','0','0','0','0',"SUM($total_sql) AS ts_total"
554
				),'GROUP BY '.$sum_sql[$type],$sum_extra_cols,$wildcard,$empty,$op,'UNION',$filter,$join,$need_full_no_count);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sum_extra_cols seems to be defined by a foreach iteration on line 531. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
555
				$sum_extra_cols[$type]{0} = '0';
556
			}
557
			$union_order[] = 'ts_start '.$sort;
558
			return parent::search('','',implode(',',$union_order),'','',false,'',$start);
559
		}
560
		return parent::search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,$start,$filter,$join,$need_full_no_count);
561
	}
562
563
	/**
564
	 * read a timesheet entry
565
	 *
566
	 * @param int $ts_id
567
	 * @param boolean $ignore_acl =false should the Acl be checked
568
	 * @return array|boolean array with timesheet entry, null if timesheet not found or false if no rights
569
	 */
570
	function read($ts_id,$ignore_acl=false)
571
	{
572
		//error_log(__METHOD__."($ts_id,$ignore_acl) ".function_backtrace());
573
		if (!(int)$ts_id || (int)$ts_id != $this->data['ts_id'] && !parent::read($ts_id))
574
		{
575
			return null;	// entry not found
576
		}
577
		if (!$ignore_acl && !($ret = $this->check_acl(Acl::READ)))
0 ignored issues
show
The assignment to $ret is dead and can be removed.
Loading history...
578
		{
579
			return false;	// no read rights
580
		}
581
		return $this->data;
582
	}
583
584
	/**
585
	 * saves a timesheet entry
586
	 *
587
	 * reimplemented to notify the link-class
588
	 *
589
	 * @param array $keys if given $keys are copied to data before saveing => allows a save as
590
	 * @param boolean $touch_modified =true should modification date+user be set, default yes
591
	 * @param boolean $ignore_acl =false should the Acl be checked, returns true if no edit-rigts
592
	 * @return int 0 on success and errno != 0 else
593
	 */
594
	function save($keys=null,$touch_modified=true,$ignore_acl=false)
595
	{
596
		if ($keys) $this->data_merge($keys);
597
598
		if (!$ignore_acl && $this->data['ts_id'] && !$this->check_acl(Acl::EDIT))
599
		{
600
			return true;
601
		}
602
		if ($touch_modified)
603
		{
604
			$this->data['ts_modifier'] = $GLOBALS['egw_info']['user']['account_id'];
605
			$this->data['ts_modified'] = $this->now;
606
			$this->user = $this->data['ts_modifier'];
607
		}
608
609
		// check if we have a real modification of an existing record
610
		if ($this->data['ts_id'])
611
		{
612
			$new =& $this->data;
613
			unset($this->data);
614
			$this->read($new['ts_id']);
615
			$old =& $this->data;
616
			$this->data =& $new;
617
			$changed = array();
618
			if (isset($old)) foreach($old as $name => $value)
619
			{
620
				if (isset($new[$name]) && $new[$name] != $value) $changed[] = $name;
621
			}
622
		}
623
		if (!$this->data['ts_created'])
624
		{
625
			$this->data['ts_created'] = time();
626
		}
627
		if (isset($old) && !$changed)
628
		{
629
			return false;
630
		}
631
		// Update ts_project to match project
632
		if ($this->pm_integration == 'full' && (
633
				!$old && $this->data['pm_id'] != $this->data['old_pm_id'] || $old && $old['pm_id'] != $new['pm_id']
634
		))
635
		{
636
			$this->data['ts_project'] = $this->data['pm_id'] ? Link::title('projectmanager', $this->data['pm_id']) : '';
637
			if($this->data['ts_title'] == Link::title('projectmanager', $old['pm_id']))
638
			{
639
				$this->data['ts_title'] = $this->data['ts_project'];
640
			}
641
		}
642
643
		$type = !isset($old) ? 'add' :
644
			($new['ts_status'] == self::DELETED_STATUS ? 'delete' : 'update');
645
646
		// Check for restore of deleted contact, restore held links
647
		if($old && $old['ts_status'] == self::DELETED_STATUS && $new['ts_status'] != self::DELETED_STATUS)
648
		{
649
			Link::restore(TIMESHEET_APP, $new['ts_id']);
650
			$type = 'add';
651
		}
652
653
		if (!($err = parent::save()))
654
		{
655
			if (!is_object($this->tracking))
656
			{
657
				$this->tracking = new timesheet_tracking($this);
658
659
				$this->tracking->html_content_allow = true;
660
			}
661
			if ($this->tracking->track($this->data,$old,$this->user) === false)
662
			{
663
				return implode(', ',$this->tracking->errors);
664
			}
665
			// notify the link-class about the update, as other apps may be subscribt to it
666
			Link::notify_update(TIMESHEET_APP, $this->data['ts_id'], $this->data, $type);
667
		}
668
669
		return $err;
670
	}
671
672
	/**
673
	 * deletes a timesheet entry identified by $keys or the loaded one, reimplemented to notify the link class (unlink)
674
	 *
675
	 * @param array $keys if given array with col => value pairs to characterise the rows to delete
676
	 * @param boolean $ignore_acl =false should the Acl be checked, returns false if no delete-rigts
677
	 * @return int affected rows, should be 1 if ok, 0 if an error
678
	 */
679
	function delete($keys=null,$ignore_acl=false)
680
	{
681
		if (!is_array($keys) && (int) $keys)
682
		{
683
			$keys = array('ts_id' => (int) $keys);
684
		}
685
		$ts_id = is_null($keys) ? $this->data['ts_id'] : $keys['ts_id'];
686
687
		if (!$ignore_acl && !$this->check_acl(Acl::DELETE,$ts_id) || !($old = $this->read($ts_id)))
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: (! $ignore_acl && ! $thi...d = $this->read($ts_id), Probably Intended Meaning: ! $ignore_acl && (! $thi... = $this->read($ts_id))
Loading history...
688
		{
689
			return false;
690
		}
691
692
		// check if we only mark timesheets as deleted, or really delete them
693
		if ($old['ts_owner'] && $this->config_data['history'] != '' && $old['ts_status'] != self::DELETED_STATUS)
694
		{
695
			$delete = $old;
696
			$delete['ts_status'] = self::DELETED_STATUS;
697
			$ret = !($this->save($delete));
698
			Link::unlink(0,TIMESHEET_APP,$ts_id,'','','',true);
699
		}
700
		elseif (($ret = parent::delete($keys)) && $ts_id)
701
		{
702
			// delete all links to timesheet entry $ts_id
703
			Link::unlink(0,TIMESHEET_APP,$ts_id);
704
		}
705
		return $ret;
706
	}
707
708
	/**
709
	 * delete / move all timesheets of a given user
710
	 *
711
	 * @param array $data
712
	 * @param int $data['account_id'] owner to change
713
	 * @param int $data['new_owner']  new owner or 0 for delete
714
	 */
715
	function deleteaccount($data)
716
	{
717
		$account_id = $data['account_id'];
718
		$new_owner =  $data['new_owner'];
719
720
		if (!$new_owner)
721
		{
722
			Link::unlink(0, TIMESHEET_APP, '', $account_id);
723
			parent::delete(array('ts_owner' => $account_id));
724
		}
725
		else
726
		{
727
			$this->db->update($this->table_name, array(
728
				'ts_owner' => $new_owner,
729
			), array(
730
				'ts_owner' => $account_id,
731
			), __LINE__, __FILE__, TIMESHEET_APP);
732
		}
733
	}
734
735
	/**
736
	 * set a status for timesheet entry identified by $keys
737
	 *
738
	 * @param array $keys =null if given array with col => value pairs to characterise single timesheet or null for $this->data
739
	 * @param int $status =0
740
	 * @return int affected rows, should be 1 if ok, 0 if an error
741
	 */
742
	function set_status($keys=null, $status=0)
743
	{
744
		$ret = true;
745
		if (!is_array($keys) && (int) $keys)
746
		{
747
			$keys = array('ts_id' => (int) $keys);
748
		}
749
		$ts_id = is_null($keys) ? $this->data['ts_id'] : $keys['ts_id'];
750
751
		if (!$this->check_acl(Acl::EDIT,$ts_id) || !$this->read($ts_id,true))
752
		{
753
			return false;
754
		}
755
756
		$this->data['ts_status'] = $status;
757
		if ($this->save($ts_id)!=0) $ret = false;
758
759
		return $ret;
760
	}
761
762
	/**
763
	 * Get the time-, price-, quantity-sum and max. modification date for the given timesheet entries
764
	 *
765
	 * @param array $ids array of timesheet id's
766
	 * @return array with values for keys "duration", "price", "max_modified" and "quantity"
767
	 */
768
	function sum($ids)
769
	{
770
		if (!$ids)
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
771
		{
772
			return array('duration' => 0, 'quantity' => 0, 'price' => 0, 'max_modified' => null);
773
		}
774
		return $this->search(array('ts_id'=>$ids),true,'','','',false,'AND',false,null,'',false,true);
775
	}
776
777
	/**
778
	 * get title for a timesheet entry identified by $entry
779
	 *
780
	 * Is called as hook to participate in the linking
781
	 *
782
	 * @param int|array $entry int ts_id or array with timesheet entry
783
	 * @return string/boolean string with title, null if timesheet not found, false if no perms to view it
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/boolean at position 0 could not be parsed: Unknown type name 'string/boolean' at position 0 in string/boolean.
Loading history...
784
	 */
785
	function link_title( $entry )
786
	{
787
		if (!is_array($entry))
788
		{
789
			// need to preserve the $this->data
790
			$backup =& $this->data;
791
			unset($this->data);
792
			$entry = $this->read( $entry,false,false);
0 ignored issues
show
The call to timesheet_bo::read() has too many arguments starting with false. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

792
			/** @scrutinizer ignore-call */ 
793
   $entry = $this->read( $entry,false,false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
793
			// restore the data again
794
			$this->data =& $backup;
795
		}
796
		if (!$entry)
797
		{
798
			return $entry;
799
		}
800
		$format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'];
801
		if (date('H:i',$entry['ts_start']) != '00:00')	// dont show 00:00 time, as it means date only
802
		{
803
			$format .= ' '.($GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] == 12 ? 'h:i a' : 'H:i');
804
		}
805
		return date($format,$entry['ts_start']).': '.$entry['ts_title'];
806
	}
807
808
	/**
809
	 * get title for multiple timesheet entries identified by $ids
810
	 *
811
	 * Is called as hook to participate in the linking
812
	 *
813
	 * @param array $ids array with ts_id's
814
	 * @return array with titles, see link_title
815
	 */
816
	function link_titles( array $ids )
817
	{
818
		$titles = array();
819
		if (($entries = $this->search(array('ts_id' => $ids),'ts_id,ts_title,ts_start')))
820
		{
821
			foreach($entries as $entry)
822
			{
823
				$titles[$entry['ts_id']] = $this->link_title($entry);
824
			}
825
		}
826
		// we assume all not returned entries are not readable by the user, as we notify Link about all deletes
827
		foreach($ids as $id)
828
		{
829
			if (!isset($titles[$id]))
830
			{
831
				$titles[$id] = false;
832
			}
833
		}
834
		return $titles;
835
	}
836
837
	/**
838
	 * query timesheet for entries matching $pattern
839
	 *
840
	 * Is called as hook to participate in the linking
841
	 *
842
	 * @param string $pattern pattern to search
843
	 * @param array $options Array of options for the search
844
	 * @return array with ts_id - title pairs of the matching entries
845
	 */
846
	function link_query( $pattern, Array &$options = array() )
847
	{
848
		$limit = false;
849
		$need_count = false;
850
		if($options['start'] || $options['num_rows']) {
851
			$limit = array($options['start'], $options['num_rows']);
852
			$need_count = true;
853
		}
854
		$result = array();
855
		foreach((array) $this->search($pattern,false,'','','%',false,'OR', $limit, null, '', $need_count) as $ts )
856
		{
857
			if ($ts) $result[$ts['ts_id']] = $this->link_title($ts);
858
		}
859
		$options['total'] = $need_count ? $this->total : count($result);
860
		return $result;
861
	}
862
863
	/**
864
	 * Check access to the file store
865
	 *
866
	 * @param int|array $id id of entry or entry array
867
	 * @param int $check Acl::READ for read and Acl::EDIT for write or delete access
868
	 * @param string $rel_path =null currently not used in InfoLog
869
	 * @param int $user =null for which user to check, default current user
870
	 * @return boolean true if access is granted or false otherwise
871
	 */
872
	function file_access($id,$check,$rel_path=null,$user=null)
873
	{
874
		unset($rel_path);	// not used, but required by function signature
875
876
		return $this->check_acl($check,$id,$user);
877
	}
878
879
	/**
880
	 * updates the project titles in the timesheet application (called whenever a project name is changed in the project manager)
881
	 *
882
	 * Todo: implement via notification
883
	 *
884
	 * @param string $oldtitle => the origin title of the project
885
	 * @param string $newtitle => the new title of the project
886
	 * @return boolean true for success, false for invalid parameters
887
	 */
888
	 function update_ts_project($oldtitle='', $newtitle='')
889
	 {
890
		if(strlen($oldtitle) > 0 && strlen($newtitle) > 0)
891
		{
892
			$this->db->update('egw_timesheet',array(
893
				'ts_project' => $newtitle,
894
				'ts_title' => $newtitle,
895
			),array(
896
				'ts_project' => $oldtitle,
897
			),__LINE__,__FILE__,TIMESHEET_APP);
898
899
			return true;
900
		}
901
		return false;
902
	 }
903
904
	/**
905
	 * returns array with relation link_id and ts_id (necessary for project-selection)
906
	 *
907
	 * @param int $pm_id ID of selected project
908
	 * @return array containing link_id and ts_id
909
	 */
910
	function get_ts_links($pm_id=0)
911
	{
912
		if($pm_id && isset($GLOBALS['egw_info']['user']['apps']['projectmanager']))
913
		{
914
			$pm_ids = ExecMethod('projectmanager.projectmanager_bo.children',$pm_id);
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod() has been deprecated: use autoloadable class-names, instanciate and call method or use static methods ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

914
			$pm_ids = /** @scrutinizer ignore-deprecated */ ExecMethod('projectmanager.projectmanager_bo.children',$pm_id);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
915
			$pm_ids[] = $pm_id;
916
			$links = Link\Storage::get_links('projectmanager',$pm_ids,'timesheet');	// Link\Storage::get_links not egw_links::get_links!
917
			if ($links)
0 ignored issues
show
Bug Best Practice introduced by
The expression $links of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
918
			{
919
				$links = array_unique(call_user_func_array('array_merge',$links));
920
			}
921
			return $links;
922
		}
923
		return array();
924
	}
925
926
	/**
927
	 * receives notifications from the link-class: new, deleted links to timesheets, or updated content of linked entries
928
	 *
929
	 * Function makes sure timesheets linked or unlinked to projects via projectmanager behave like ones
930
	 * linked via timesheets project-selector, thought timesheet only stores project-title, not the id!
931
	 *
932
	 * @param array $data array with keys type, id, target_app, target_id, link_id, data
933
	 */
934
	function notify($data)
935
	{
936
		//error_log(__METHOD__.'('.array2string($data).')');
937
		$backup =& $this->data;	// backup internal data in case class got re-used by ExecMethod
938
		unset($this->data);
939
940
		if ($data['target_app'] == 'projectmanager' && $this->read($data['id']))
941
		{
942
			$old_title = isset($data['data']) ? $data['data'][Link::OLD_LINK_TITLE] : null;
943
			switch($data['type'])
944
			{
945
				case 'link':
946
				case 'update':
947
					if (empty($this->data['ts_project']) ||	// timesheet has not yet project set --> set just linked one
948
						isset($old_title) && $this->data['ts_project'] === $old_title)
949
					{
950
						$pm_id = $data['target_id'];
951
						$update['ts_project'] = Link::title('projectmanager', $pm_id);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$update was never initialized. Although not strictly required by PHP, it is generally a good practice to add $update = array(); before regardless.
Loading history...
952
						if (isset($old_title) && $this->data['ts_title'] === $old_title)
953
						{
954
							$update['ts_title'] = $update['ts_project'];
955
						}
956
					}
957
					break;
958
959
				case 'unlink':	// if current project got unlinked --> unset it
960
					if ($this->data['ts_project'] == projectmanager_bo::link_title($data['target_id']))
0 ignored issues
show
The type projectmanager_bo was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
961
					{
962
						$pm_id = 0;
0 ignored issues
show
The assignment to $pm_id is dead and can be removed.
Loading history...
963
						$update['ts_project'] = null;
964
965
					}
966
					break;
967
			}
968
			if (isset($update))
969
			{
970
				$this->update($update);
971
				// do NOT notify about title-change, as this will lead to an infinit loop!
972
				// Link::notify_update(TIMESHEET_APP, $this->data['ts_id'],$this->data);
973
				//error_log(__METHOD__."() setting pm_id=$pm_id --> ".array2string($update));
974
			}
975
		}
976
		if ($backup) $this->data = $backup;
0 ignored issues
show
Bug Best Practice introduced by
The expression $backup of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
977
	}
978
979
980
	/**
981
	 * changes the data from the db-format to your work-format
982
	 *
983
	 * Reimplemented to store just ts_project in db, but have pm_id and ts_project in memory,
984
	 * with ts_project only set, if it contains a custom project name.
985
	 *
986
	 * @param array $data =null if given works on that array and returns result, else works on internal data-array
987
	 * @return array
988
	 */
989
	function db2data($data=null)
990
	{
991
		if (($intern = !is_array($data)))
992
		{
993
			$data =& $this->data;
994
		}
995
		// get pm_id from links and ts_project: either project matching ts_project or first found project
996
		if (!isset($data['pm_id']) && $data['ts_id'])
997
		{
998
			$first_pm_id = null;
999
			foreach(Link::get_links('timesheet', $data['ts_id'], 'projectmanager') as $pm_id)
1000
			{
1001
				if (!isset($first_pm_id)) $first_pm_id = $pm_id;
1002
				if ($data['ts_project'] == Link::title('projectmanager', $pm_id))
1003
				{
1004
					$data['pm_id'] = $pm_id;
1005
					$data['ts_project_blur'] = $data['ts_project'];
1006
					$data['ts_project'] = '';
1007
					break;
1008
				}
1009
			}
1010
			if (!isset($data['pm_id']) && isset($first_pm_id)) $data['pm_id'] = $first_pm_id;
1011
		}
1012
		elseif ($data['ts_id'] && $data['pm_id'] && Link::title('projectmanager', $data['pm_id']) == $data['ts_project'])
1013
		{
1014
			$data['ts_project_blur'] = $data['ts_project'];
1015
			$data['ts_project'] = '';
1016
		}
1017
		return parent::db2data($intern ? null : $data);	// important to use null, if $intern!
1018
	}
1019
1020
	/**
1021
	 * changes the data from your work-format to the db-format
1022
	 *
1023
	 * Reimplemented to store just ts_project in db, but have pm_id and ts_project in memory,
1024
	 * with ts_project only set, if it contains a custom project name.
1025
	 *
1026
	 * @param array $data =null if given works on that array and returns result, else works on internal data-array
1027
	 * @return array
1028
	 */
1029
	function data2db($data=null)
1030
	{
1031
		if (($intern = !is_array($data)))
1032
		{
1033
			$data =& $this->data;
1034
		}
1035
		// allways store ts_project to be able to search for it, even if no custom project is set
1036
		if (empty($data['ts_project']) && !is_null($data['ts_project']))
1037
		{
1038
			$data['ts_project'] = $data['pm_id'] ? Link::title('projectmanager', $data['pm_id']) : '';
1039
		}
1040
		return parent::data2db($intern ? null : $data);	// important to use null, if $intern!
1041
	}
1042
}
1043