1<?php
2/* Copyright (C) 2002-2005 Rodolphe Quiedeville <rodolphe@quiedeville.org>
3 * Copyright (C) 2005-2020 Laurent Destailleur  <eldy@users.sourceforge.net>
4 * Copyright (C) 2005-2010 Regis Houssin        <regis.houssin@inodbox.com>
5 * Copyright (C) 2013	   Florian Henry        <florian.henry@open-concept.pro>
6 * Copyright (C) 2014-2017 Marcos García        <marcosgdf@gmail.com>
7 * Copyright (C) 2017      Ferran Marcet        <fmarcet@2byte.es>
8 * Copyright (C) 2019      Juanjo Menent        <jmenent@2byte.es>
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 3 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 */
23
24/**
25 * 		\file       htdocs/projet/class/project.class.php
26 * 		\ingroup    projet
27 * 		\brief      File of class to manage projects
28 */
29require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
30
31/**
32 *	Class to manage projects
33 */
34class Project extends CommonObject
35{
36
37	/**
38	 * @var string ID to identify managed object
39	 */
40	public $element = 'project';
41
42	/**
43	 * @var string Name of table without prefix where object is stored
44	 */
45	public $table_element = 'projet';
46
47	/**
48	 * @var string    Name of subtable line
49	 */
50	public $table_element_line = 'projet_task';
51
52	/**
53	 * @var string    Name of field date
54	 */
55	public $table_element_date;
56
57	/**
58	 * @var string Field with ID of parent key if this field has a parent
59	 */
60	public $fk_element = 'fk_projet';
61
62	/**
63	 * 0=No test on entity, 1=Test with field entity, 2=Test with link by societe
64	 * @var int
65	 */
66	public $ismultientitymanaged = 1;
67
68	/**
69	 * @var string String with name of icon for myobject. Must be the part after the 'object_' into object_myobject.png
70	 */
71	public $picto = 'project';
72
73	/**
74	 * {@inheritdoc}
75	 */
76	protected $table_ref_field = 'ref';
77
78	/**
79	 * @var string description
80	 */
81	public $description;
82
83	/**
84	 * @var string
85	 */
86	public $title;
87
88	public $date_start;
89	public $date_end;
90	public $date_close;
91
92	public $socid; // To store id of thirdparty
93	public $thirdparty_name; // To store name of thirdparty (defined only in some cases)
94
95	public $user_author_id; //!< Id of project creator. Not defined if shared project.
96
97	/**
98	 * @var int user close id
99	 */
100	public $fk_user_close;
101
102	/**
103	 * @var int user close id
104	 */
105	public $user_close_id;
106	public $public; //!< Tell if this is a public or private project
107
108	/**
109	 * @var float budget Amount
110	 */
111	public $budget_amount;
112
113	/**
114	 * @var integer		Can use projects to follow opportunities
115	 */
116	public $usage_opportunity;
117
118	/**
119	 * @var integer		Can follow tasks on project and enter time spent on it
120	 */
121	public $usage_task;
122
123	/**
124	 * @var integer	 	Use to bill task spend time
125	 */
126	public $usage_bill_time; // Is the time spent on project must be invoiced or not
127
128	/**
129	   * @var integer		Event organization: Use Event Organization
130	   */
131	public $usage_organize_event;
132
133	/**
134	 * @var integer		Event organization: Allow unknown people to suggest new conferences
135	 */
136	public $accept_conference_suggestions;
137
138	/**
139	 * @var integer		Event organization: Allow unknown people to suggest new booth
140	 */
141	public $accept_booth_suggestions;
142
143	/**
144	 * @var float Event organization: registration price
145	 */
146	public $price_registration;
147
148	/**
149	 * @var float Event organization: booth price
150	 */
151	public $price_booth;
152
153	public $statuts_short;
154	public $statuts_long;
155
156	public $statut; // 0=draft, 1=opened, 2=closed
157	public $opp_status; // opportunity status, into table llx_c_lead_status
158	public $opp_percent; // opportunity probability
159
160	public $email_msgid;
161
162	public $oldcopy;
163
164	public $weekWorkLoad; // Used to store workload details of a projet
165	public $weekWorkLoadPerTask; // Used to store workload details of tasks of a projet
166
167	/**
168	 * @var int Creation date
169	 * @deprecated
170	 * @see $date_c
171	 */
172	public $datec;
173
174	/**
175	 * @var int Creation date
176	 */
177	public $date_c;
178
179	/**
180	 * @var int Modification date
181	 * @deprecated
182	 * @see $date_m
183	 */
184	public $datem;
185
186	/**
187	 * @var int Modification date
188	 */
189	public $date_m;
190
191	/**
192	 * @var Task[]
193	 */
194	public $lines;
195
196	/**
197	 *  'type' if the field format ('integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter]]', 'varchar(x)', 'double(24,8)', 'real', 'price', 'text', 'html', 'date', 'datetime', 'timestamp', 'duration', 'mail', 'phone', 'url', 'password')
198	 *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)"
199	 *  'label' the translation key.
200	 *  'enabled' is a condition when the field must be managed.
201	 *  'position' is the sort order of field.
202	 *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
203	 *  'visible' says if field is visible in list (Examples: 0=Not visible, 1=Visible on list and create/update/view forms, 2=Visible on list only, 3=Visible on create/update/view form only (not list), 4=Visible on list and update/view form only (not create). 5=Visible on list and view only (not create/not update). Using a negative value means field is not shown by default on list but can be selected for viewing)
204	 *  'noteditable' says if field is not editable (1 or 0)
205	 *  'default' is a default value for creation (can still be overwrote by the Setup of Default Values if field is editable in creation form). Note: If default is set to '(PROV)' and field is 'ref', the default value will be set to '(PROVid)' where id is rowid when a new record is created.
206	 *  'index' if we want an index in database.
207	 *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommanded to name the field fk_...).
208	 *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
209	 *  'isameasure' must be set to 1 if you want to have a total on list for this field. Field type must be summable like integer or double(24,8).
210	 *  'css' is the CSS style to use on field. For example: 'maxwidth200'
211	 *  'help' is a string visible as a tooltip on field
212	 *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
213	 *  'disabled' is 1 if we want to have the field locked by a 'disabled' attribute. In most cases, this is never set into the definition of $fields into class, but is set dynamically by some part of code.
214	 *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
215	 *  'comment' is not used. You can store here any text of your choice. It is not used by application.
216	 *
217	 *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
218	 */
219
220	// BEGIN MODULEBUILDER PROPERTIES
221	/**
222	 * @var array  Array with all fields and their property. Do not use it as a static var. It may be modified by constructor.
223	 */
224	public $fields = array(
225		'rowid' =>array('type'=>'integer', 'label'=>'ID', 'enabled'=>1, 'visible'=>-1, 'notnull'=>1, 'position'=>10),
226		'ref' =>array('type'=>'varchar(50)', 'label'=>'Ref', 'enabled'=>1, 'visible'=>1, 'showoncombobox'=>1, 'position'=>15, 'searchall'=>1),
227		'title' =>array('type'=>'varchar(255)', 'label'=>'ProjectLabel', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'position'=>17, 'showoncombobox'=>2, 'searchall'=>1),
228		'entity' =>array('type'=>'integer', 'label'=>'Entity', 'default'=>1, 'enabled'=>1, 'visible'=>3, 'notnull'=>1, 'position'=>19),
229		'fk_soc' =>array('type'=>'integer', 'label'=>'Thirdparty', 'enabled'=>1, 'visible'=>0, 'position'=>20),
230		'dateo' =>array('type'=>'date', 'label'=>'DateStart', 'enabled'=>1, 'visible'=>1, 'position'=>30),
231		'datee' =>array('type'=>'date', 'label'=>'DateEnd', 'enabled'=>1, 'visible'=>1, 'position'=>35),
232		'description' =>array('type'=>'text', 'label'=>'Description', 'enabled'=>1, 'visible'=>3, 'position'=>55, 'searchall'=>1),
233		'public' =>array('type'=>'integer', 'label'=>'Visibility', 'enabled'=>1, 'visible'=>1, 'position'=>65),
234		'fk_opp_status' =>array('type'=>'integer', 'label'=>'OpportunityStatusShort', 'enabled'=>1, 'visible'=>1, 'position'=>75),
235		'opp_percent' =>array('type'=>'double(5,2)', 'label'=>'OpportunityProbabilityShort', 'enabled'=>1, 'visible'=>1, 'position'=>80),
236		'note_private' =>array('type'=>'text', 'label'=>'NotePrivate', 'enabled'=>1, 'visible'=>0, 'position'=>85, 'searchall'=>1),
237		'note_public' =>array('type'=>'text', 'label'=>'NotePublic', 'enabled'=>1, 'visible'=>0, 'position'=>90, 'searchall'=>1),
238		'model_pdf' =>array('type'=>'varchar(255)', 'label'=>'ModelPdf', 'enabled'=>1, 'visible'=>0, 'position'=>95),
239		'date_close' =>array('type'=>'datetime', 'label'=>'DateClosing', 'enabled'=>1, 'visible'=>0, 'position'=>105),
240		'fk_user_close' =>array('type'=>'integer', 'label'=>'UserClosing', 'enabled'=>1, 'visible'=>0, 'position'=>110),
241		'opp_amount' =>array('type'=>'double(24,8)', 'label'=>'OpportunityAmountShort', 'enabled'=>1, 'visible'=>1, 'position'=>115),
242		'budget_amount' =>array('type'=>'double(24,8)', 'label'=>'Budget', 'enabled'=>1, 'visible'=>1, 'position'=>119),
243		'usage_bill_time' =>array('type'=>'integer', 'label'=>'UsageBillTimeShort', 'enabled'=>1, 'visible'=>-1, 'position'=>130),
244		'usage_opportunity' =>array('type'=>'integer', 'label'=>'UsageOpportunity', 'enabled'=>1, 'visible'=>-1, 'position'=>135),
245		'usage_task' =>array('type'=>'integer', 'label'=>'UsageTasks', 'enabled'=>1, 'visible'=>-1, 'position'=>140),
246		'usage_organize_event' =>array('type'=>'integer', 'label'=>'UsageOrganizeEvent', 'enabled'=>1, 'visible'=>-1, 'position'=>145),
247		'accept_conference_suggestions' =>array('type'=>'integer', 'label'=>'AllowUnknownPeopleSuggestConf', 'enabled'=>1, 'visible'=>-1, 'position'=>146),
248		'accept_booth_suggestions' =>array('type'=>'integer', 'label'=>'AllowUnknownPeopleSuggestBooth', 'enabled'=>1, 'visible'=>-1, 'position'=>147),
249		'price_registration' =>array('type'=>'double(24,8)', 'label'=>'PriceOfRegistration', 'enabled'=>1, 'visible'=>-1, 'position'=>148),
250		'price_booth' =>array('type'=>'double(24,8)', 'label'=>'PriceOfBooth', 'enabled'=>1, 'visible'=>-1, 'position'=>149),
251		'datec' =>array('type'=>'datetime', 'label'=>'DateCreationShort', 'enabled'=>1, 'visible'=>-2, 'position'=>200),
252		'tms' =>array('type'=>'timestamp', 'label'=>'DateModificationShort', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>205),
253		'fk_user_creat' =>array('type'=>'integer', 'label'=>'UserCreation', 'enabled'=>1, 'visible'=>0, 'notnull'=>1, 'position'=>210),
254		'fk_user_modif' =>array('type'=>'integer', 'label'=>'UserModification', 'enabled'=>1, 'visible'=>0, 'position'=>215),
255		'import_key' =>array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>0, 'position'=>220),
256		'email_msgid'=>array('type'=>'varchar(255)', 'label'=>'EmailMsgID', 'enabled'=>1, 'visible'=>-1, 'position'=>250, 'help'=>'EmailMsgIDWhenSourceisEmail'),
257		'fk_statut' =>array('type'=>'smallint(6)', 'label'=>'Status', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'position'=>500)
258	);
259	// END MODULEBUILDER PROPERTIES
260
261	/**
262	 * Draft status
263	 */
264	const STATUS_DRAFT = 0;
265
266	/**
267	 * Open/Validated status
268	 */
269	const STATUS_VALIDATED = 1;
270
271	/**
272	 * Closed status
273	 */
274	const STATUS_CLOSED = 2;
275
276	/**
277	 *  Constructor
278	 *
279	 *  @param      DoliDB		$db      Database handler
280	 */
281	public function __construct($db)
282	{
283		global $conf;
284
285		$this->db = $db;
286
287		$this->statuts_short = array(0 => 'Draft', 1 => 'Opened', 2 => 'Closed');
288		$this->statuts_long = array(0 => 'Draft', 1 => 'Opened', 2 => 'Closed');
289
290		global $conf;
291
292		if (empty($conf->global->MAIN_SHOW_TECHNICAL_ID)) {
293			$this->fields['rowid']['visible'] = 0;
294		}
295
296		if (empty($conf->global->PROJECT_USE_OPPORTUNITIES)) {
297			$this->fields['fk_opp_status']['enabled'] = 0;
298			$this->fields['opp_percent']['enabled'] = 0;
299			$this->fields['opp_amount']['enabled'] = 0;
300			$this->fields['usage_opportunity']['enabled'] = 0;
301		}
302
303		if (!empty($conf->global->PROJECT_HIDE_TASKS)) {
304			$this->fields['usage_bill_time']['visible'] = 0;
305			$this->fields['usage_task']['visible'] = 0;
306		}
307
308		if (empty($conf->eventorganization->enabled)) {
309			$this->fields['usage_organize_event']['visible'] = 0;
310			$this->fields['accept_conference_suggestions']['enabled'] = 0;
311			$this->fields['accept_booth_suggestions']['enabled'] = 0;
312			$this->fields['price_registration']['enabled'] = 0;
313			$this->fields['price_booth']['enabled'] = 0;
314		}
315	}
316
317	/**
318	 *    Create a project into database
319	 *
320	 *    @param    User	$user       	User making creation
321	 *    @param	int		$notrigger		Disable triggers
322	 *    @return   int         			<0 if KO, id of created project if OK
323	 */
324	public function create($user, $notrigger = 0)
325	{
326		global $conf, $langs;
327
328		$error = 0;
329		$ret = 0;
330
331		$now = dol_now();
332
333		// Clean parameters
334		$this->note_private = dol_substr($this->note_private, 0, 65535);
335		$this->note_public = dol_substr($this->note_public, 0, 65535);
336
337		// Check parameters
338		if (!trim($this->ref)) {
339			$this->error = 'ErrorFieldsRequired';
340			dol_syslog(get_class($this)."::create error -1 ref null", LOG_ERR);
341			return -1;
342		}
343		if (!empty($conf->global->PROJECT_THIRDPARTY_REQUIRED) && !($this->socid > 0)) {
344			$this->error = 'ErrorFieldsRequired';
345			dol_syslog(get_class($this)."::create error -1 thirdparty not defined and option PROJECT_THIRDPARTY_REQUIRED is set", LOG_ERR);
346			return -1;
347		}
348
349		// Create project
350		$this->db->begin();
351
352		$sql = "INSERT INTO ".MAIN_DB_PREFIX."projet (";
353		$sql .= "ref";
354		$sql .= ", title";
355		$sql .= ", description";
356		$sql .= ", fk_soc";
357		$sql .= ", fk_user_creat";
358		$sql .= ", fk_statut";
359		$sql .= ", fk_opp_status";
360		$sql .= ", opp_percent";
361		$sql .= ", public";
362		$sql .= ", datec";
363		$sql .= ", dateo";
364		$sql .= ", datee";
365		$sql .= ", opp_amount";
366		$sql .= ", budget_amount";
367		$sql .= ", usage_opportunity";
368		$sql .= ", usage_task";
369		$sql .= ", usage_bill_time";
370		$sql .= ", usage_organize_event";
371		$sql .= ", accept_conference_suggestions";
372		$sql .= ", accept_booth_suggestions";
373		$sql .= ", price_registration";
374		$sql .= ", price_booth";
375		$sql .= ", email_msgid";
376		$sql .= ", note_private";
377		$sql .= ", note_public";
378		$sql .= ", entity";
379		$sql .= ") VALUES (";
380		$sql .= "'".$this->db->escape($this->ref)."'";
381		$sql .= ", '".$this->db->escape($this->title)."'";
382		$sql .= ", '".$this->db->escape($this->description)."'";
383		$sql .= ", ".($this->socid > 0 ? $this->socid : "null");
384		$sql .= ", ".$user->id;
385		$sql .= ", ".(is_numeric($this->statut) ? $this->statut : '0');
386		$sql .= ", ".((is_numeric($this->opp_status) && $this->opp_status > 0) ? $this->opp_status : 'NULL');
387		$sql .= ", ".(is_numeric($this->opp_percent) ? $this->opp_percent : 'NULL');
388		$sql .= ", ".($this->public ? 1 : 0);
389		$sql .= ", '".$this->db->idate($now)."'";
390		$sql .= ", ".($this->date_start != '' ? "'".$this->db->idate($this->date_start)."'" : 'null');
391		$sql .= ", ".($this->date_end != '' ? "'".$this->db->idate($this->date_end)."'" : 'null');
392		$sql .= ", ".(strcmp($this->opp_amount, '') ? price2num($this->opp_amount) : 'null');
393		$sql .= ", ".(strcmp($this->budget_amount, '') ? price2num($this->budget_amount) : 'null');
394		$sql .= ", ".($this->usage_opportunity ? 1 : 0);
395		$sql .= ", ".($this->usage_task ? 1 : 0);
396		$sql .= ", ".($this->usage_bill_time ? 1 : 0);
397		$sql .= ", ".($this->usage_organize_event ? 1 : 0);
398		$sql .= ", ".($this->accept_conference_suggestions ? 1 : 0);
399		$sql .= ", ".($this->accept_booth_suggestions ? 1 : 0);
400		$sql .= ", ".(strcmp($this->price_registration, '') ? price2num($this->price_registration) : 'null');
401		$sql .= ", ".(strcmp($this->price_booth, '') ? price2num($this->price_booth) : 'null');
402		$sql .= ", ".($this->email_msgid ? "'".$this->db->escape($this->email_msgid)."'" : 'null');
403		$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : 'null');
404		$sql .= ", ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : 'null');
405		$sql .= ", ".$conf->entity;
406		$sql .= ")";
407
408		dol_syslog(get_class($this)."::create", LOG_DEBUG);
409		$resql = $this->db->query($sql);
410		if ($resql) {
411			$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."projet");
412			$ret = $this->id;
413
414			if (!$notrigger) {
415				// Call trigger
416				$result = $this->call_trigger('PROJECT_CREATE', $user);
417				if ($result < 0) {
418					$error++;
419				}
420				// End call triggers
421			}
422		} else {
423			$this->error = $this->db->lasterror();
424			$this->errno = $this->db->lasterrno();
425			$error++;
426		}
427
428		// Update extrafield
429		if (!$error) {
430			$result = $this->insertExtraFields();
431			if ($result < 0) {
432				$error++;
433			}
434		}
435
436		if (!$error && !empty($conf->global->MAIN_DISABLEDRAFTSTATUS)) {
437			$res = $this->setValid($user);
438			if ($res < 0) {
439				$error++;
440			}
441		}
442
443		if (!$error) {
444			$this->db->commit();
445			return $ret;
446		} else {
447			$this->db->rollback();
448			return -1;
449		}
450	}
451
452	/**
453	 * Update a project
454	 *
455	 * @param  User		$user       User object of making update
456	 * @param  int		$notrigger  1=Disable all triggers
457	 * @return int                  <=0 if KO, >0 if OK
458	 */
459	public function update($user, $notrigger = 0)
460	{
461		global $langs, $conf;
462
463		$error = 0;
464
465		// Clean parameters
466		$this->title = trim($this->title);
467		$this->description = trim($this->description);
468		if ($this->opp_amount < 0) {
469			$this->opp_amount = '';
470		}
471		if ($this->opp_percent < 0) {
472			$this->opp_percent = '';
473		}
474		if ($this->date_end && $this->date_end < $this->date_start) {
475			$this->error = $langs->trans("ErrorDateEndLowerThanDateStart");
476			$this->errors[] = $this->error;
477			$this->db->rollback();
478			dol_syslog(get_class($this)."::update error -3 ".$this->error, LOG_ERR);
479			return -3;
480		}
481
482		if (dol_strlen(trim($this->ref)) > 0) {
483			$this->db->begin();
484
485			$sql = "UPDATE ".MAIN_DB_PREFIX."projet SET";
486			$sql .= " ref='".$this->db->escape($this->ref)."'";
487			$sql .= ", title = '".$this->db->escape($this->title)."'";
488			$sql .= ", description = '".$this->db->escape($this->description)."'";
489			$sql .= ", fk_soc = ".($this->socid > 0 ? $this->socid : "null");
490			$sql .= ", fk_statut = ".((int) $this->statut);
491			$sql .= ", fk_opp_status = ".((is_numeric($this->opp_status) && $this->opp_status > 0) ? $this->opp_status : 'null');
492			$sql .= ", opp_percent = ".((is_numeric($this->opp_percent) && $this->opp_percent != '') ? $this->opp_percent : 'null');
493			$sql .= ", public = ".($this->public ? 1 : 0);
494			$sql .= ", datec=".($this->date_c != '' ? "'".$this->db->idate($this->date_c)."'" : 'null');
495			$sql .= ", dateo=".($this->date_start != '' ? "'".$this->db->idate($this->date_start)."'" : 'null');
496			$sql .= ", datee=".($this->date_end != '' ? "'".$this->db->idate($this->date_end)."'" : 'null');
497			$sql .= ", date_close=".($this->date_close != '' ? "'".$this->db->idate($this->date_close)."'" : 'null');
498			$sql .= ", fk_user_close=".($this->fk_user_close > 0 ? $this->fk_user_close : "null");
499			$sql .= ", opp_amount = ".(strcmp($this->opp_amount, '') ? price2num($this->opp_amount) : "null");
500			$sql .= ", budget_amount = ".(strcmp($this->budget_amount, '') ? price2num($this->budget_amount) : "null");
501			$sql .= ", fk_user_modif = ".$user->id;
502			$sql .= ", usage_opportunity = ".($this->usage_opportunity ? 1 : 0);
503			$sql .= ", usage_task = ".($this->usage_task ? 1 : 0);
504			$sql .= ", usage_bill_time = ".($this->usage_bill_time ? 1 : 0);
505			$sql .= ", usage_organize_event = ".($this->usage_organize_event ? 1 : 0);
506			$sql .= ", accept_conference_suggestions = ".($this->accept_conference_suggestions ? 1 : 0);
507			$sql .= ", accept_booth_suggestions = ".($this->accept_booth_suggestions ? 1 : 0);
508			$sql .= ", price_registration = ".(strcmp($this->price_registration, '') ? price2num($this->price_registration) : "null");
509			$sql .= ", price_booth = ".(strcmp($this->price_booth, '') ? price2num($this->price_booth) : "null");
510			$sql .= " WHERE rowid = ".((int) $this->id);
511
512			dol_syslog(get_class($this)."::update", LOG_DEBUG);
513			$resql = $this->db->query($sql);
514			if ($resql) {
515				// Update extrafield
516				if (!$error) {
517					$result = $this->insertExtraFields();
518					if ($result < 0) {
519						$error++;
520					}
521				}
522
523				if (!$error && !$notrigger) {
524					// Call trigger
525					$result = $this->call_trigger('PROJECT_MODIFY', $user);
526					if ($result < 0) {
527						$error++;
528					}
529					// End call triggers
530				}
531
532				if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
533					// We remove directory
534					if ($conf->projet->dir_output) {
535						$olddir = $conf->projet->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
536						$newdir = $conf->projet->dir_output."/".dol_sanitizeFileName($this->ref);
537						if (file_exists($olddir)) {
538							include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
539							$res = @rename($olddir, $newdir);
540							if (!$res) {
541								$langs->load("errors");
542								$this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
543								$error++;
544							}
545						}
546					}
547				}
548				if (!$error) {
549					$this->db->commit();
550					$result = 1;
551				} else {
552					$this->db->rollback();
553					$result = -1;
554				}
555			} else {
556				$this->error = $this->db->lasterror();
557				$this->errors[] = $this->error;
558				$this->db->rollback();
559				if ($this->db->lasterrno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
560					$result = -4;
561				} else {
562					$result = -2;
563				}
564				dol_syslog(get_class($this)."::update error ".$result." ".$this->error, LOG_ERR);
565			}
566		} else {
567			dol_syslog(get_class($this)."::update ref null");
568			$result = -1;
569		}
570
571		return $result;
572	}
573
574	/**
575	 * 	Get object from database
576	 *
577	 * 	@param      int		$id       		Id of object to load
578	 * 	@param		string	$ref			Ref of project
579	 * 	@param		string	$ref_ext		Ref ext of project
580	 *  @param		string	$email_msgid	Email msgid
581	 * 	@return     int      		   		>0 if OK, 0 if not found, <0 if KO
582	 */
583	public function fetch($id, $ref = '', $ref_ext = '', $email_msgid = '')
584	{
585		global $conf;
586
587		if (empty($id) && empty($ref) && empty($ref_ext) && empty($email_msgid)) {
588			dol_syslog(get_class($this)."::fetch Bad parameters", LOG_WARNING);
589			return -1;
590		}
591
592		$sql = "SELECT rowid, entity, ref, title, description, public, datec, opp_amount, budget_amount,";
593		$sql .= " tms, dateo, datee, date_close, fk_soc, fk_user_creat, fk_user_modif, fk_user_close, fk_statut as status, fk_opp_status, opp_percent,";
594		$sql .= " note_private, note_public, model_pdf, usage_opportunity, usage_task, usage_bill_time, usage_organize_event, email_msgid,";
595		$sql .= " accept_conference_suggestions, accept_booth_suggestions, price_registration, price_booth";
596		$sql .= " FROM ".MAIN_DB_PREFIX."projet";
597		if (!empty($id)) {
598			$sql .= " WHERE rowid = ".((int) $id);
599		} else {
600			$sql .= " WHERE entity IN (".getEntity('project').")";
601			if (!empty($ref)) {
602				$sql .= " AND ref = '".$this->db->escape($ref)."'";
603			} elseif (!empty($ref_ext)) {
604				$sql .= " AND ref_ext = '".$this->db->escape($ref_ext)."'";
605			} else {
606				$sql .= " AND email_msgid = '".$this->db->escape($email_msgid)."'";
607			}
608		}
609
610		dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
611		$resql = $this->db->query($sql);
612		if ($resql) {
613			$num_rows = $this->db->num_rows($resql);
614
615			if ($num_rows) {
616				$obj = $this->db->fetch_object($resql);
617
618				$this->id = $obj->rowid;
619				$this->entity = $obj->entity;
620				$this->ref = $obj->ref;
621				$this->title = $obj->title;
622				$this->description = $obj->description;
623				$this->date_c = $this->db->jdate($obj->datec);
624				$this->datec = $this->db->jdate($obj->datec); // TODO deprecated
625				$this->date_m = $this->db->jdate($obj->tms);
626				$this->datem = $this->db->jdate($obj->tms); // TODO deprecated
627				$this->date_start = $this->db->jdate($obj->dateo);
628				$this->date_end = $this->db->jdate($obj->datee);
629				$this->date_close = $this->db->jdate($obj->date_close);
630				$this->note_private = $obj->note_private;
631				$this->note_public = $obj->note_public;
632				$this->socid = $obj->fk_soc;
633				$this->user_author_id = $obj->fk_user_creat;
634				$this->user_modification_id = $obj->fk_user_modif;
635				$this->user_close_id = $obj->fk_user_close;
636				$this->public = $obj->public;
637				$this->statut = $obj->status; // deprecated
638				$this->status = $obj->status;
639				$this->opp_status = $obj->fk_opp_status;
640				$this->opp_amount	= $obj->opp_amount;
641				$this->opp_percent = $obj->opp_percent;
642				$this->budget_amount = $obj->budget_amount;
643				$this->model_pdf = $obj->model_pdf;
644				$this->modelpdf = $obj->model_pdf; // deprecated
645				$this->usage_opportunity = (int) $obj->usage_opportunity;
646				$this->usage_task = (int) $obj->usage_task;
647				$this->usage_bill_time = (int) $obj->usage_bill_time;
648				$this->usage_organize_event = (int) $obj->usage_organize_event;
649				$this->accept_conference_suggestions = (int) $obj->accept_conference_suggestions;
650				$this->accept_booth_suggestions = (int) $obj->accept_booth_suggestions;
651				$this->price_registration = $obj->price_registration;
652				$this->price_booth = $obj->price_booth;
653				$this->email_msgid = $obj->email_msgid;
654
655				$this->db->free($resql);
656
657				// Retrieve all extrafield
658				// fetch optionals attributes and labels
659				$this->fetch_optionals();
660
661				return 1;
662			}
663
664			$this->db->free($resql);
665
666			return 0;
667		} else {
668			$this->error = $this->db->lasterror();
669			$this->errors[] = $this->db->lasterror();
670			return -1;
671		}
672	}
673
674	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
675	/**
676	 * 	Return list of elements for type, linked to a project
677	 *
678	 * 	@param		string		$type			'propal','order','invoice','order_supplier','invoice_supplier',...
679	 * 	@param		string		$tablename		name of table associated of the type
680	 * 	@param		string		$datefieldname	name of date field for filter
681	 *  @param		int			$dates			Start date
682	 *  @param		int			$datee			End date
683	 *	@param		string		$projectkey		Equivalent key  to fk_projet for actual type
684	 * 	@return		mixed						Array list of object ids linked to project, < 0 or string if error
685	 */
686	public function get_element_list($type, $tablename, $datefieldname = '', $dates = '', $datee = '', $projectkey = 'fk_projet')
687	{
688		// phpcs:enable
689
690		global $hookmanager;
691
692		$elements = array();
693
694		if ($this->id <= 0) {
695			return $elements;
696		}
697
698		$ids = $this->id;
699
700		if ($type == 'agenda') {
701			$sql = "SELECT id as rowid FROM ".MAIN_DB_PREFIX."actioncomm WHERE fk_project IN (".$this->db->sanitize($ids).") AND entity IN (".getEntity('agenda').")";
702		} elseif ($type == 'expensereport') {
703			$sql = "SELECT ed.rowid FROM ".MAIN_DB_PREFIX."expensereport as e, ".MAIN_DB_PREFIX."expensereport_det as ed WHERE e.rowid = ed.fk_expensereport AND e.entity IN (".getEntity('expensereport').") AND ed.fk_projet IN (".$this->db->sanitize($ids).")";
704		} elseif ($type == 'project_task') {
705			$sql = "SELECT DISTINCT pt.rowid FROM ".MAIN_DB_PREFIX."projet_task as pt WHERE pt.fk_projet IN (".$this->db->sanitize($ids).")";
706		} elseif ($type == 'project_task_time') {	// Case we want to duplicate line foreach user
707			$sql = "SELECT DISTINCT pt.rowid, ptt.fk_user FROM ".MAIN_DB_PREFIX."projet_task as pt, ".MAIN_DB_PREFIX."projet_task_time as ptt WHERE pt.rowid = ptt.fk_task AND pt.fk_projet IN (".$this->db->sanitize($ids).")";
708		} elseif ($type == 'stock_mouvement') {
709			$sql = 'SELECT ms.rowid, ms.fk_user_author as fk_user FROM '.MAIN_DB_PREFIX."stock_mouvement as ms, ".MAIN_DB_PREFIX."entrepot as e WHERE e.rowid = ms.fk_entrepot AND e.entity IN (".getEntity('stock').") AND ms.origintype = 'project' AND ms.fk_origin IN (".$this->db->sanitize($ids).") AND ms.type_mouvement = 1";
710		} elseif ($type == 'loan') {
711			$sql = 'SELECT l.rowid, l.fk_user_author as fk_user FROM '.MAIN_DB_PREFIX."loan as l WHERE l.entity IN (".getEntity('loan').") AND l.fk_projet IN (".$this->db->sanitize($ids).")";
712		} else {
713			$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tablename." WHERE ".$projectkey." IN (".$this->db->sanitize($ids).") AND entity IN (".getEntity($type).")";
714		}
715
716		if ($dates > 0 && $type == 'loan') {
717			$sql .= " AND (dateend > '".$this->db->idate($dates)."' OR dateend IS NULL)";
718		} elseif ($dates > 0 && ($type != 'project_task')) {	// For table project_taks, we want the filter on date apply on project_time_spent table
719			if (empty($datefieldname) && !empty($this->table_element_date)) {
720				$datefieldname = $this->table_element_date;
721			}
722			if (empty($datefieldname)) {
723				return 'Error this object has no date field defined';
724			}
725			$sql .= " AND (".$datefieldname." >= '".$this->db->idate($dates)."' OR ".$datefieldname." IS NULL)";
726		}
727
728		if ($datee > 0 && $type == 'loan') {
729			$sql .= " AND (datestart < '".$this->db->idate($datee)."' OR datestart IS NULL)";
730		} elseif ($datee > 0 && ($type != 'project_task')) {	// For table project_taks, we want the filter on date apply on project_time_spent table
731			if (empty($datefieldname) && !empty($this->table_element_date)) {
732				$datefieldname = $this->table_element_date;
733			}
734			if (empty($datefieldname)) {
735				return 'Error this object has no date field defined';
736			}
737			$sql .= " AND (".$datefieldname." <= '".$this->db->idate($datee)."' OR ".$datefieldname." IS NULL)";
738		}
739
740		$parameters = array(
741			'sql'=>$sql,
742			'type' => $type,
743			'tablename' => $tablename,
744			'datefieldname'  => $datefieldname,
745			'dates' => $dates,
746			'datee' => $datee,
747			'fk_projet' => $projectkey
748		);
749		$reshook = $hookmanager->executeHooks('getElementList', $parameters, $object, $action);
750		if ($reshook > 0) {
751			$sql = $hookmanager->resPrint;
752		} else {
753			$sql .= $hookmanager->resPrint;
754		}
755
756		if (!$sql) {
757			return -1;
758		}
759
760		//print $sql;
761		dol_syslog(get_class($this)."::get_element_list", LOG_DEBUG);
762		$result = $this->db->query($sql);
763		if ($result) {
764			$nump = $this->db->num_rows($result);
765			if ($nump) {
766				$i = 0;
767				while ($i < $nump) {
768					$obj = $this->db->fetch_object($result);
769
770					$elements[$i] = $obj->rowid.(empty($obj->fk_user) ? '' : '_'.$obj->fk_user);
771
772					$i++;
773				}
774				$this->db->free($result);
775			}
776
777			/* Return array even if empty*/
778			return $elements;
779		} else {
780			dol_print_error($this->db);
781		}
782	}
783
784	/**
785	 *    Delete a project from database
786	 *
787	 *    @param       User		$user            User
788	 *    @param       int		$notrigger       Disable triggers
789	 *    @return      int       			      <0 if KO, 0 if not possible, >0 if OK
790	 */
791	public function delete($user, $notrigger = 0)
792	{
793		global $langs, $conf;
794		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
795
796		$error = 0;
797
798		$this->db->begin();
799
800		if (!$error) {
801			// Delete linked contacts
802			$res = $this->delete_linked_contact();
803			if ($res < 0) {
804				$this->error = 'ErrorFailToDeleteLinkedContact';
805				//$error++;
806				$this->db->rollback();
807				return 0;
808			}
809		}
810
811		// Set fk_projet into elements to null
812		$listoftables = array(
813			'propal'=>'fk_projet', 'commande'=>'fk_projet', 'facture'=>'fk_projet',
814			'supplier_proposal'=>'fk_projet', 'commande_fournisseur'=>'fk_projet', 'facture_fourn'=>'fk_projet',
815			'expensereport_det'=>'fk_projet', 'contrat'=>'fk_projet', 'fichinter'=>'fk_projet', 'don'=>'fk_projet',
816			'actioncomm'=>'fk_project', 'mrp_mo'=>'fk_project', 'entrepot'=>'fk_project'
817		);
818		foreach ($listoftables as $key => $value) {
819			$sql = "UPDATE ".MAIN_DB_PREFIX.$key." SET ".$value." = NULL where ".$value." = ".((int) $this->id);
820			$resql = $this->db->query($sql);
821			if (!$resql) {
822				$this->errors[] = $this->db->lasterror();
823				$error++;
824				break;
825			}
826		}
827
828		// Remove linked categories.
829		if (!$error) {
830			$sql = "DELETE FROM ".MAIN_DB_PREFIX."categorie_project";
831			$sql .= " WHERE fk_project = ".((int) $this->id);
832
833			$result = $this->db->query($sql);
834			if (!$result) {
835				$error++;
836				$this->errors[] = $this->db->lasterror();
837			}
838		}
839
840		// Fetch tasks
841		$this->getLinesArray($user);
842
843		// Delete tasks
844		$ret = $this->deleteTasks($user);
845		if ($ret < 0) {
846			$error++;
847		}
848
849
850		// Delete all child tables
851		if (!$error) {
852			$elements = array('categorie_project'); // elements to delete. TODO Make goodway to delete
853			foreach ($elements as $table) {
854				if (!$error) {
855					$sql = "DELETE FROM ".MAIN_DB_PREFIX.$table;
856					$sql .= " WHERE fk_project = ".((int) $this->id);
857
858					$result = $this->db->query($sql);
859					if (!$result) {
860						$error++;
861						$this->errors[] = $this->db->lasterror();
862					}
863				}
864			}
865		}
866
867		if (!$error) {
868			$sql = "DELETE FROM ".MAIN_DB_PREFIX."projet_extrafields";
869			$sql .= " WHERE fk_object=".$this->id;
870
871			$resql = $this->db->query($sql);
872			if (!$resql) {
873				$this->errors[] = $this->db->lasterror();
874				$error++;
875			}
876		}
877
878		// Delete project
879		if (!$error) {
880			$sql = "DELETE FROM ".MAIN_DB_PREFIX."projet";
881			$sql .= " WHERE rowid=".((int) $this->id);
882
883			$resql = $this->db->query($sql);
884			if (!$resql) {
885				$this->errors[] = $langs->trans("CantRemoveProject", $langs->transnoentitiesnoconv("ProjectOverview"));
886				$error++;
887			}
888		}
889
890
891
892		if (empty($error)) {
893			// We remove directory
894			$projectref = dol_sanitizeFileName($this->ref);
895			if ($conf->projet->dir_output) {
896				$dir = $conf->projet->dir_output."/".$projectref;
897				if (file_exists($dir)) {
898					$res = @dol_delete_dir_recursive($dir);
899					if (!$res) {
900						$this->errors[] = 'ErrorFailToDeleteDir';
901						$error++;
902					}
903				}
904			}
905
906			if (!$notrigger) {
907				// Call trigger
908				$result = $this->call_trigger('PROJECT_DELETE', $user);
909
910				if ($result < 0) {
911					$error++;
912				}
913				// End call triggers
914			}
915		}
916
917		if (empty($error)) {
918			$this->db->commit();
919			return 1;
920		} else {
921			foreach ($this->errors as $errmsg) {
922				dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
923				$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
924			}
925			dol_syslog(get_class($this)."::delete ".$this->error, LOG_ERR);
926			$this->db->rollback();
927			return -1;
928		}
929	}
930
931	/**
932	 * Return the count of a type of linked elements of this project
933	 *
934	 * @param string	$type			The type of the linked elements (e.g. 'propal', 'order', 'invoice', 'order_supplier', 'invoice_supplier')
935	 * @param string	$tablename		The name of table associated of the type
936	 * @param string	$projectkey 	(optional) Equivalent key to fk_projet for actual type
937	 * @return integer					The count of the linked elements (the count is zero on request error too)
938	 */
939	public function getElementCount($type, $tablename, $projectkey = 'fk_projet')
940	{
941		if ($this->id <= 0) {
942			return 0;
943		}
944
945		if ($type == 'agenda') {
946			$sql = "SELECT COUNT(id) as nb FROM ".MAIN_DB_PREFIX."actioncomm WHERE fk_project = ".$this->id." AND entity IN (".getEntity('agenda').")";
947		} elseif ($type == 'expensereport') {
948			$sql = "SELECT COUNT(ed.rowid) as nb FROM ".MAIN_DB_PREFIX."expensereport as e, ".MAIN_DB_PREFIX."expensereport_det as ed WHERE e.rowid = ed.fk_expensereport AND e.entity IN (".getEntity('expensereport').") AND ed.fk_projet = ".((int) $this->id);
949		} elseif ($type == 'project_task') {
950			$sql = "SELECT DISTINCT COUNT(pt.rowid) as nb FROM ".MAIN_DB_PREFIX."projet_task as pt WHERE pt.fk_projet = ".((int) $this->id);
951		} elseif ($type == 'project_task_time') {	// Case we want to duplicate line foreach user
952			$sql = "SELECT DISTINCT COUNT(pt.rowid) as nb FROM ".MAIN_DB_PREFIX."projet_task as pt, ".MAIN_DB_PREFIX."projet_task_time as ptt WHERE pt.rowid = ptt.fk_task AND pt.fk_projet = ".((int) $this->id);
953		} elseif ($type == 'stock_mouvement') {
954			$sql = 'SELECT COUNT(ms.rowid) as nb FROM '.MAIN_DB_PREFIX."stock_mouvement as ms, ".MAIN_DB_PREFIX."entrepot as e WHERE e.rowid = ms.fk_entrepot AND e.entity IN (".getEntity('stock').") AND ms.origintype = 'project' AND ms.fk_origin = ".((int) $this->id)." AND ms.type_mouvement = 1";
955		} elseif ($type == 'loan') {
956			$sql = 'SELECT COUNT(l.rowid) as nb FROM '.MAIN_DB_PREFIX."loan as l WHERE l.entity IN (".getEntity('loan').") AND l.fk_projet = ".((int) $this->id);
957		} else {
958			$sql = "SELECT COUNT(rowid) as nb FROM ".MAIN_DB_PREFIX.$tablename." WHERE ".$projectkey." = ".((int) $this->id)." AND entity IN (".getEntity($type).")";
959		}
960
961		$result = $this->db->query($sql);
962
963		if (!$result) {
964			return 0;
965		}
966
967		$obj = $this->db->fetch_object($result);
968
969		$this->db->free($result);
970
971		return $obj->nb;
972	}
973
974	/**
975	 * 		Delete tasks with no children first, then task with children recursively
976	 *
977	 *  	@param     	User		$user		User
978	 *		@return		int				<0 if KO, 1 if OK
979	 */
980	public function deleteTasks($user)
981	{
982		$countTasks = count($this->lines);
983		$deleted = false;
984		if ($countTasks) {
985			foreach ($this->lines as $task) {
986				if ($task->hasChildren() <= 0) {		// If there is no children (or error to detect them)
987					$deleted = true;
988					$ret = $task->delete($user);
989					if ($ret <= 0) {
990						$this->errors[] = $this->db->lasterror();
991						return -1;
992					}
993				}
994			}
995		}
996		$this->getLinesArray($user);
997		if ($deleted && count($this->lines) < $countTasks) {
998			if (count($this->lines)) {
999				$this->deleteTasks($this->lines);
1000			}
1001		}
1002
1003		return 1;
1004	}
1005
1006	/**
1007	 * 		Validate a project
1008	 *
1009	 * 		@param		User	$user		   User that validate
1010	 *      @param      int     $notrigger     1=Disable triggers
1011	 * 		@return		int					   <0 if KO, >0 if OK
1012	 */
1013	public function setValid($user, $notrigger = 0)
1014	{
1015		global $langs, $conf;
1016
1017		$error = 0;
1018
1019		if ($this->statut != 1) {
1020			// Check parameters
1021			if (preg_match('/^'.preg_quote($langs->trans("CopyOf").' ').'/', $this->title)) {
1022				$this->error = $langs->trans("ErrorFieldFormat", $langs->transnoentities("Label")).'. '.$langs->trans('RemoveString', $langs->transnoentitiesnoconv("CopyOf"));
1023				return -1;
1024			}
1025
1026			$this->db->begin();
1027
1028			$sql = "UPDATE ".MAIN_DB_PREFIX."projet";
1029			$sql .= " SET fk_statut = 1";
1030			$sql .= " WHERE rowid = ".((int) $this->id);
1031			$sql .= " AND entity = ".((int) $conf->entity);
1032
1033			dol_syslog(get_class($this)."::setValid", LOG_DEBUG);
1034			$resql = $this->db->query($sql);
1035			if ($resql) {
1036				// Call trigger
1037				if (empty($notrigger)) {
1038					$result = $this->call_trigger('PROJECT_VALIDATE', $user);
1039					if ($result < 0) {
1040						$error++;
1041					}
1042					// End call triggers
1043				}
1044
1045				if (!$error) {
1046					$this->statut = 1;
1047					$this->db->commit();
1048					return 1;
1049				} else {
1050					$this->db->rollback();
1051					$this->error = join(',', $this->errors);
1052					dol_syslog(get_class($this)."::setValid ".$this->error, LOG_ERR);
1053					return -1;
1054				}
1055			} else {
1056				$this->db->rollback();
1057				$this->error = $this->db->lasterror();
1058				return -1;
1059			}
1060		}
1061	}
1062
1063	/**
1064	 * 		Close a project
1065	 *
1066	 * 		@param		User	$user		User that close project
1067	 * 		@return		int					<0 if KO, 0 if already closed, >0 if OK
1068	 */
1069	public function setClose($user)
1070	{
1071		global $langs, $conf;
1072
1073		$now = dol_now();
1074
1075		$error = 0;
1076
1077		if ($this->statut != self::STATUS_CLOSED) {
1078			$this->db->begin();
1079
1080			$sql = "UPDATE ".MAIN_DB_PREFIX."projet";
1081			$sql .= " SET fk_statut = ".self::STATUS_CLOSED.", fk_user_close = ".((int) $user->id).", date_close = '".$this->db->idate($now)."'";
1082			$sql .= " WHERE rowid = ".$this->id;
1083			$sql .= " AND fk_statut = ".self::STATUS_VALIDATED;
1084
1085			if (!empty($conf->global->PROJECT_USE_OPPORTUNITIES)) {
1086				// TODO What to do if fk_opp_status is not code 'WON' or 'LOST'
1087			}
1088
1089			dol_syslog(get_class($this)."::setClose", LOG_DEBUG);
1090			$resql = $this->db->query($sql);
1091			if ($resql) {
1092				// Call trigger
1093				$result = $this->call_trigger('PROJECT_CLOSE', $user);
1094				if ($result < 0) {
1095					$error++;
1096				}
1097				// End call triggers
1098
1099				if (!$error) {
1100					$this->statut = 2;
1101					$this->db->commit();
1102					return 1;
1103				} else {
1104					$this->db->rollback();
1105					$this->error = join(',', $this->errors);
1106					dol_syslog(get_class($this)."::setClose ".$this->error, LOG_ERR);
1107					return -1;
1108				}
1109			} else {
1110				$this->db->rollback();
1111				$this->error = $this->db->lasterror();
1112				return -1;
1113			}
1114		}
1115
1116		return 0;
1117	}
1118
1119	/**
1120	 *  Return status label of object
1121	 *
1122	 *  @param  int			$mode       0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto
1123	 * 	@return string      			Label
1124	 */
1125	public function getLibStatut($mode = 0)
1126	{
1127		return $this->LibStatut(isset($this->statut) ? $this->statut : $this->status, $mode);
1128	}
1129
1130	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1131	/**
1132	 *  Renvoi status label for a status
1133	 *
1134	 *  @param	int		$status     id status
1135	 *  @param  int		$mode       0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
1136	 * 	@return string				Label
1137	 */
1138	public function LibStatut($status, $mode = 0)
1139	{
1140		// phpcs:enable
1141		global $langs;
1142
1143		$statustrans = array(
1144			0 => 'status0',
1145			1 => 'status4',
1146			2 => 'status6',
1147		);
1148
1149		$statusClass = 'status0';
1150		if (!empty($statustrans[$status])) {
1151			$statusClass = $statustrans[$status];
1152		}
1153
1154		return dolGetStatus($langs->trans($this->statuts_long[$status]), $langs->trans($this->statuts_short[$status]), '', $statusClass, $mode);
1155	}
1156
1157	/**
1158	 * 	Return clickable name (with picto eventually)
1159	 *
1160	 * 	@param	int		$withpicto		          0=No picto, 1=Include picto into link, 2=Only picto
1161	 * 	@param	string	$option			          Variant where the link point to ('', 'nolink')
1162	 * 	@param	int		$addlabel		          0=Default, 1=Add label into string, >1=Add first chars into string
1163	 *  @param	string	$moreinpopup	          Text to add into popup
1164	 *  @param	string	$sep			          Separator between ref and label if option addlabel is set
1165	 *  @param	int   	$notooltip		          1=Disable tooltip
1166	 *  @param  int     $save_lastsearch_value    -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
1167	 *  @param	string	$morecss				  More css on a link
1168	 * 	@return	string					          String with URL
1169	 */
1170	public function getNomUrl($withpicto = 0, $option = '', $addlabel = 0, $moreinpopup = '', $sep = ' - ', $notooltip = 0, $save_lastsearch_value = -1, $morecss = '')
1171	{
1172		global $conf, $langs, $user, $hookmanager;
1173
1174		if (!empty($conf->dol_no_mouse_hover)) {
1175			$notooltip = 1; // Force disable tooltips
1176		}
1177
1178		$result = '';
1179		if (!empty($conf->global->PROJECT_OPEN_ALWAYS_ON_TAB)) {
1180			$option = $conf->global->PROJECT_OPEN_ALWAYS_ON_TAB;
1181		}
1182
1183		$label = '';
1184		if ($option != 'nolink') {
1185			$label = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("Project").'</u>';
1186		}
1187		if (isset($this->status)) {
1188			$label .= ' '.$this->getLibStatut(5);
1189		}
1190		$label .= ($label ? '<br>' : '').'<b>'.$langs->trans('Ref').': </b>'.$this->ref; // The space must be after the : to not being explode when showing the title in img_picto
1191		$label .= ($label ? '<br>' : '').'<b>'.$langs->trans('Label').': </b>'.$this->title; // The space must be after the : to not being explode when showing the title in img_picto
1192		if (isset($this->public)) {
1193			$label .= '<br><b>'.$langs->trans("Visibility").":</b> ".($this->public ? $langs->trans("SharedProject") : $langs->trans("PrivateProject"));
1194		}
1195		if (!empty($this->thirdparty_name)) {
1196			$label .= ($label ? '<br>' : '').'<b>'.$langs->trans('ThirdParty').': </b>'.$this->thirdparty_name; // The space must be after the : to not being explode when showing the title in img_picto
1197		}
1198		if (!empty($this->dateo)) {
1199			$label .= ($label ? '<br>' : '').'<b>'.$langs->trans('DateStart').': </b>'.dol_print_date($this->dateo, 'day'); // The space must be after the : to not being explode when showing the title in img_picto
1200		}
1201		if (!empty($this->datee)) {
1202			$label .= ($label ? '<br>' : '').'<b>'.$langs->trans('DateEnd').': </b>'.dol_print_date($this->datee, 'day'); // The space must be after the : to not being explode when showing the title in img_picto
1203		}
1204		if ($moreinpopup) {
1205			$label .= '<br>'.$moreinpopup;
1206		}
1207
1208		$url = '';
1209		if ($option != 'nolink') {
1210			if (preg_match('/\.php$/', $option)) {
1211				$url = dol_buildpath($option, 1).'?id='.$this->id;
1212			} elseif ($option == 'task') {
1213				$url = DOL_URL_ROOT.'/projet/tasks.php?id='.$this->id;
1214			} elseif ($option == 'preview') {
1215				$url = DOL_URL_ROOT.'/projet/element.php?id='.$this->id;
1216			} elseif ($option == 'eventorganization') {
1217				$url = DOL_URL_ROOT.'/eventorganization/conferenceorbooth_list.php?projectid='.$this->id;
1218			} else {
1219				$url = DOL_URL_ROOT.'/projet/card.php?id='.$this->id;
1220			}
1221			// Add param to save lastsearch_values or not
1222			$add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1223			if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1224				$add_save_lastsearch_values = 1;
1225			}
1226			if ($add_save_lastsearch_values) {
1227				$url .= '&save_lastsearch_values=1';
1228			}
1229		}
1230
1231		$linkclose = '';
1232		if (empty($notooltip) && $user->rights->projet->lire) {
1233			if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
1234				$label = $langs->trans("ShowProject");
1235				$linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
1236			}
1237			$linkclose .= ' title="'.dol_escape_htmltag($label, 1).'"';
1238			$linkclose .= ' class="classfortooltip'.($morecss ? ' '.$morecss : '').'"';
1239		} else {
1240			$linkclose = ($morecss ? ' class="'.$morecss.'"' : '');
1241		}
1242
1243		$picto = 'projectpub';
1244		if (!$this->public) {
1245			$picto = 'project';
1246		}
1247
1248		$linkstart = '<a href="'.$url.'"';
1249		$linkstart .= $linkclose.'>';
1250		$linkend = '</a>';
1251
1252		$result .= $linkstart;
1253		if ($withpicto) {
1254			$result .= img_object(($notooltip ? '' : $label), $picto, ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'classfortooltip"'), 0, 0, $notooltip ? 0 : 1);
1255		}
1256		if ($withpicto != 2) {
1257			$result .= $this->ref;
1258		}
1259		$result .= $linkend;
1260		if ($withpicto != 2) {
1261			$result .= (($addlabel && $this->title) ? $sep.dol_trunc($this->title, ($addlabel > 1 ? $addlabel : 0)) : '');
1262		}
1263
1264		global $action;
1265		$hookmanager->initHooks(array('projectdao'));
1266		$parameters = array('id'=>$this->id, 'getnomurl'=>$result);
1267		$reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1268		if ($reshook > 0) {
1269			$result = $hookmanager->resPrint;
1270		} else {
1271			$result .= $hookmanager->resPrint;
1272		}
1273
1274		return $result;
1275	}
1276
1277	/**
1278	 *  Initialise an instance with random values.
1279	 *  Used to build previews or test instances.
1280	 * 	id must be 0 if object instance is a specimen.
1281	 *
1282	 *  @return	void
1283	 */
1284	public function initAsSpecimen()
1285	{
1286		global $user, $langs, $conf;
1287
1288		$now = dol_now();
1289
1290		// Initialise parameters
1291		$this->id = 0;
1292		$this->ref = 'SPECIMEN';
1293		$this->specimen = 1;
1294		$this->socid = 1;
1295		$this->date_c = $now;
1296		$this->date_m = $now;
1297		$this->date_start = $now;
1298		$this->date_end = $now + (3600 * 24 * 365);
1299		$this->note_public = 'SPECIMEN';
1300		$this->fk_ele = 20000;
1301		$this->opp_amount = 20000;
1302		$this->budget_amount = 10000;
1303
1304		$this->usage_opportunity = 1;
1305		$this->usage_task = 1;
1306		$this->usage_bill_time = 1;
1307		$this->usage_organize_event = 1;
1308
1309		/*
1310		 $nbp = mt_rand(1, 9);
1311		 $xnbp = 0;
1312		 while ($xnbp < $nbp)
1313		 {
1314		 $line = new Task($this->db);
1315		 $line->fk_project = 0;
1316		 $line->label = $langs->trans("Label") . " " . $xnbp;
1317		 $line->description = $langs->trans("Description") . " " . $xnbp;
1318
1319		 $this->lines[]=$line;
1320		 $xnbp++;
1321		 }
1322		 */
1323	}
1324
1325	/**
1326	 * 	Check if user has permission on current project
1327	 *
1328	 * 	@param	User	$user		Object user to evaluate
1329	 * 	@param  string	$mode		Type of permission we want to know: 'read', 'write'
1330	 * 	@return	int					>0 if user has permission, <0 if user has no permission
1331	 */
1332	public function restrictedProjectArea(User $user, $mode = 'read')
1333	{
1334		// To verify role of users
1335		$userAccess = 0;
1336		if (($mode == 'read' && !empty($user->rights->projet->all->lire)) || ($mode == 'write' && !empty($user->rights->projet->all->creer)) || ($mode == 'delete' && !empty($user->rights->projet->all->supprimer))) {
1337			$userAccess = 1;
1338		} elseif ($this->public && (($mode == 'read' && !empty($user->rights->projet->lire)) || ($mode == 'write' && !empty($user->rights->projet->creer)) || ($mode == 'delete' && !empty($user->rights->projet->supprimer)))) {
1339			$userAccess = 1;
1340		} else {	// No access due to permission to read all projects, so we check if we are a contact of project
1341			foreach (array('internal', 'external') as $source) {
1342				$userRole = $this->liste_contact(4, $source);
1343				$num = count($userRole);
1344
1345				$nblinks = 0;
1346				while ($nblinks < $num) {
1347					if ($source == 'internal' && $user->id == $userRole[$nblinks]['id']) {	// $userRole[$nblinks]['id'] is id of user (llx_user) for internal contacts
1348						if ($mode == 'read' && $user->rights->projet->lire) {
1349							$userAccess++;
1350						}
1351						if ($mode == 'write' && $user->rights->projet->creer) {
1352							$userAccess++;
1353						}
1354						if ($mode == 'delete' && $user->rights->projet->supprimer) {
1355							$userAccess++;
1356						}
1357					}
1358					if ($source == 'external' && $user->socid > 0 && $user->socid == $userRole[$nblinks]['socid']) {	// $userRole[$nblinks]['id'] is id of contact (llx_socpeople) or external contacts
1359						if ($mode == 'read' && $user->rights->projet->lire) {
1360							$userAccess++;
1361						}
1362						if ($mode == 'write' && $user->rights->projet->creer) {
1363							$userAccess++;
1364						}
1365						if ($mode == 'delete' && $user->rights->projet->supprimer) {
1366							$userAccess++;
1367						}
1368					}
1369					$nblinks++;
1370				}
1371			}
1372			//if (empty($nblinks))	// If nobody has permission, we grant creator
1373			//{
1374			//	if ((!empty($this->user_author_id) && $this->user_author_id == $user->id))
1375			//	{
1376			//		$userAccess = 1;
1377			//	}
1378			//}
1379		}
1380
1381		return ($userAccess ? $userAccess : -1);
1382	}
1383
1384	/**
1385	 * Return array of projects a user has permission on, is affected to, or all projects
1386	 *
1387	 * @param 	User	$user			User object
1388	 * @param 	int		$mode			0=All project I have permission on (assigned to me or public), 1=Projects assigned to me only, 2=Will return list of all projects with no test on contacts
1389	 * @param 	int		$list			0=Return array, 1=Return string list
1390	 * @param	int		$socid			0=No filter on third party, id of third party
1391	 * @param	string	$filter			additionnal filter on project (statut, ref, ...)
1392	 * @return 	array or string			Array of projects id, or string with projects id separated with "," if list is 1
1393	 */
1394	public function getProjectsAuthorizedForUser($user, $mode = 0, $list = 0, $socid = 0, $filter = '')
1395	{
1396		$projects = array();
1397		$temp = array();
1398
1399		$sql = "SELECT ".(($mode == 0 || $mode == 1) ? "DISTINCT " : "")."p.rowid, p.ref";
1400		$sql .= " FROM ".MAIN_DB_PREFIX."projet as p";
1401		if ($mode == 0) {
1402			$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."element_contact as ec ON ec.element_id = p.rowid";
1403		} elseif ($mode == 1) {
1404			$sql .= ", ".MAIN_DB_PREFIX."element_contact as ec";
1405		} elseif ($mode == 2) {
1406			// No filter. Use this if user has permission to see all project
1407		}
1408		$sql .= " WHERE p.entity IN (".getEntity('project').")";
1409		// Internal users must see project he is contact to even if project linked to a third party he can't see.
1410		//if ($socid || ! $user->rights->societe->client->voir)	$sql.= " AND (p.fk_soc IS NULL OR p.fk_soc = 0 OR p.fk_soc = ".((int) $socid).")";
1411		if ($socid > 0) {
1412			$sql .= " AND (p.fk_soc IS NULL OR p.fk_soc = 0 OR p.fk_soc = ".((int) $socid).")";
1413		}
1414
1415		// Get id of types of contacts for projects (This list never contains a lot of elements)
1416		$listofprojectcontacttype = array();
1417		$sql2 = "SELECT ctc.rowid, ctc.code FROM ".MAIN_DB_PREFIX."c_type_contact as ctc";
1418		$sql2 .= " WHERE ctc.element = '".$this->db->escape($this->element)."'";
1419		$sql2 .= " AND ctc.source = 'internal'";
1420		$resql = $this->db->query($sql2);
1421		if ($resql) {
1422			while ($obj = $this->db->fetch_object($resql)) {
1423				$listofprojectcontacttype[$obj->rowid] = $obj->code;
1424			}
1425		} else {
1426			dol_print_error($this->db);
1427		}
1428		if (count($listofprojectcontacttype) == 0) {
1429			$listofprojectcontacttype[0] = '0'; // To avoid syntax error if not found
1430		}
1431
1432		if ($mode == 0) {
1433			$sql .= " AND ( p.public = 1";
1434			$sql .= " OR ( ec.fk_c_type_contact IN (".$this->db->sanitize(join(',', array_keys($listofprojectcontacttype))).")";
1435			$sql .= " AND ec.fk_socpeople = ".((int) $user->id).")";
1436			$sql .= " )";
1437		} elseif ($mode == 1) {
1438			$sql .= " AND ec.element_id = p.rowid";
1439			$sql .= " AND (";
1440			$sql .= "  ( ec.fk_c_type_contact IN (".$this->db->sanitize(join(',', array_keys($listofprojectcontacttype))).")";
1441			$sql .= " AND ec.fk_socpeople = ".((int) $user->id).")";
1442			$sql .= " )";
1443		} elseif ($mode == 2) {
1444			// No filter. Use this if user has permission to see all project
1445		}
1446
1447		$sql .= $filter;
1448		//print $sql;
1449
1450		$resql = $this->db->query($sql);
1451		if ($resql) {
1452			$num = $this->db->num_rows($resql);
1453			$i = 0;
1454			while ($i < $num) {
1455				$row = $this->db->fetch_row($resql);
1456				$projects[$row[0]] = $row[1];
1457				$temp[] = $row[0];
1458				$i++;
1459			}
1460
1461			$this->db->free($resql);
1462
1463			if ($list) {
1464				if (empty($temp)) {
1465					return '0';
1466				}
1467				$result = implode(',', $temp);
1468				return $result;
1469			}
1470		} else {
1471			dol_print_error($this->db);
1472		}
1473
1474		return $projects;
1475	}
1476
1477	/**
1478	 * Load an object from its id and create a new one in database
1479	 *
1480	 *  @param	User	$user		          User making the clone
1481	 *  @param	int		$fromid     	      Id of object to clone
1482	 *  @param	bool	$clone_contact	      Clone contact of project
1483	 *  @param	bool	$clone_task		      Clone task of project
1484	 *  @param	bool	$clone_project_file	  Clone file of project
1485	 *  @param	bool	$clone_task_file	  Clone file of task (if task are copied)
1486	 *  @param	bool	$clone_note		      Clone note of project
1487	 *  @param	bool	$move_date		      Move task date on clone
1488	 *  @param	integer	$notrigger		      No trigger flag
1489	 *  @param  int     $newthirdpartyid      New thirdparty id
1490	 *  @return	int						      New id of clone
1491	 */
1492	public function createFromClone(User $user, $fromid, $clone_contact = false, $clone_task = true, $clone_project_file = false, $clone_task_file = false, $clone_note = true, $move_date = true, $notrigger = 0, $newthirdpartyid = 0)
1493	{
1494		global $langs, $conf;
1495
1496		$error = 0;
1497
1498		dol_syslog("createFromClone clone_contact=".$clone_contact." clone_task=".$clone_task." clone_project_file=".$clone_project_file." clone_note=".$clone_note." move_date=".$move_date, LOG_DEBUG);
1499
1500		$now = dol_mktime(0, 0, 0, idate('m', dol_now()), idate('d', dol_now()), idate('Y', dol_now()));
1501
1502		$clone_project = new Project($this->db);
1503
1504		$clone_project->context['createfromclone'] = 'createfromclone';
1505
1506		$this->db->begin();
1507
1508		// Load source object
1509		$clone_project->fetch($fromid);
1510		$clone_project->fetch_optionals();
1511		if ($newthirdpartyid > 0) {
1512			$clone_project->socid = $newthirdpartyid;
1513		}
1514		$clone_project->fetch_thirdparty();
1515
1516		$orign_dt_start = $clone_project->date_start;
1517		$orign_project_ref = $clone_project->ref;
1518
1519		$clone_project->id = 0;
1520		if ($move_date) {
1521			$clone_project->date_start = $now;
1522			if (!(empty($clone_project->date_end))) {
1523				$clone_project->date_end = $clone_project->date_end + ($now - $orign_dt_start);
1524			}
1525		}
1526
1527		$clone_project->date_c = $now;
1528
1529		if (!$clone_note) {
1530			$clone_project->note_private = '';
1531			$clone_project->note_public = '';
1532		}
1533
1534		//Generate next ref
1535		$defaultref = '';
1536		$obj = empty($conf->global->PROJECT_ADDON) ? 'mod_project_simple' : $conf->global->PROJECT_ADDON;
1537		// Search template files
1538		$file = ''; $classname = ''; $filefound = 0;
1539		$dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
1540		foreach ($dirmodels as $reldir) {
1541			$file = dol_buildpath($reldir."core/modules/project/".$obj.'.php', 0);
1542			if (file_exists($file)) {
1543				$filefound = 1;
1544				dol_include_once($reldir."core/modules/project/".$obj.'.php');
1545				$modProject = new $obj;
1546				$defaultref = $modProject->getNextValue(is_object($clone_project->thirdparty) ? $clone_project->thirdparty : null, $clone_project);
1547				break;
1548			}
1549		}
1550		if (is_numeric($defaultref) && $defaultref <= 0) {
1551			$defaultref = '';
1552		}
1553
1554		$clone_project->ref = $defaultref;
1555		$clone_project->title = $langs->trans("CopyOf").' '.$clone_project->title;
1556
1557		// Create clone
1558		$result = $clone_project->create($user, $notrigger);
1559
1560		// Other options
1561		if ($result < 0) {
1562			$this->error .= $clone_project->error;
1563			$error++;
1564		}
1565
1566		if (!$error) {
1567			//Get the new project id
1568			$clone_project_id = $clone_project->id;
1569
1570			//Note Update
1571			if (!$clone_note) {
1572				$clone_project->note_private = '';
1573				$clone_project->note_public = '';
1574			} else {
1575				$this->db->begin();
1576				$res = $clone_project->update_note(dol_html_entity_decode($clone_project->note_public, ENT_QUOTES | ENT_HTML5), '_public');
1577				if ($res < 0) {
1578					$this->error .= $clone_project->error;
1579					$error++;
1580					$this->db->rollback();
1581				} else {
1582					$this->db->commit();
1583				}
1584
1585				$this->db->begin();
1586				$res = $clone_project->update_note(dol_html_entity_decode($clone_project->note_private, ENT_QUOTES | ENT_HTML5), '_private');
1587				if ($res < 0) {
1588					$this->error .= $clone_project->error;
1589					$error++;
1590					$this->db->rollback();
1591				} else {
1592					$this->db->commit();
1593				}
1594			}
1595
1596			//Duplicate contact
1597			if ($clone_contact) {
1598				$origin_project = new Project($this->db);
1599				$origin_project->fetch($fromid);
1600
1601				foreach (array('internal', 'external') as $source) {
1602					$tab = $origin_project->liste_contact(-1, $source);
1603
1604					foreach ($tab as $contacttoadd) {
1605						$clone_project->add_contact($contacttoadd['id'], $contacttoadd['code'], $contacttoadd['source'], $notrigger);
1606						if ($clone_project->error == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1607							$langs->load("errors");
1608							$this->error .= $langs->trans("ErrorThisContactIsAlreadyDefinedAsThisType");
1609							$error++;
1610						} else {
1611							if ($clone_project->error != '') {
1612								$this->error .= $clone_project->error;
1613								$error++;
1614							}
1615						}
1616					}
1617				}
1618			}
1619
1620			//Duplicate file
1621			if ($clone_project_file) {
1622				require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1623
1624				$clone_project_dir = $conf->projet->dir_output."/".dol_sanitizeFileName($defaultref);
1625				$ori_project_dir = $conf->projet->dir_output."/".dol_sanitizeFileName($orign_project_ref);
1626
1627				if (dol_mkdir($clone_project_dir) >= 0) {
1628					$filearray = dol_dir_list($ori_project_dir, "files", 0, '', '(\.meta|_preview.*\.png)$', '', SORT_ASC, 1);
1629					foreach ($filearray as $key => $file) {
1630						$rescopy = dol_copy($ori_project_dir.'/'.$file['name'], $clone_project_dir.'/'.$file['name'], 0, 1);
1631						if (is_numeric($rescopy) && $rescopy < 0) {
1632							$this->error .= $langs->trans("ErrorFailToCopyFile", $ori_project_dir.'/'.$file['name'], $clone_project_dir.'/'.$file['name']);
1633							$error++;
1634						}
1635					}
1636				} else {
1637					$this->error .= $langs->trans('ErrorInternalErrorDetected').':dol_mkdir';
1638					$error++;
1639				}
1640			}
1641
1642			//Duplicate task
1643			if ($clone_task) {
1644				require_once DOL_DOCUMENT_ROOT.'/projet/class/task.class.php';
1645
1646				$taskstatic = new Task($this->db);
1647
1648				// Security check
1649				$socid = 0;
1650				if ($user->socid > 0) {
1651					$socid = $user->socid;
1652				}
1653
1654				$tasksarray = $taskstatic->getTasksArray(0, 0, $fromid, $socid, 0);
1655
1656				$tab_conv_child_parent = array();
1657
1658				// Loop on each task, to clone it
1659				foreach ($tasksarray as $tasktoclone) {
1660					$result_clone = $taskstatic->createFromClone($user, $tasktoclone->id, $clone_project_id, $tasktoclone->fk_parent, $move_date, true, false, $clone_task_file, true, false);
1661					if ($result_clone <= 0) {
1662						$this->error .= $result_clone->error;
1663						$error++;
1664					} else {
1665						$new_task_id = $result_clone;
1666						$taskstatic->fetch($tasktoclone->id);
1667
1668						//manage new parent clone task id
1669						// if the current task has child we store the original task id and the equivalent clone task id
1670						if (($taskstatic->hasChildren()) && !array_key_exists($tasktoclone->id, $tab_conv_child_parent)) {
1671							$tab_conv_child_parent[$tasktoclone->id] = $new_task_id;
1672						}
1673					}
1674				}
1675
1676				//Parse all clone node to be sure to update new parent
1677				$tasksarray = $taskstatic->getTasksArray(0, 0, $clone_project_id, $socid, 0);
1678				foreach ($tasksarray as $task_cloned) {
1679					$taskstatic->fetch($task_cloned->id);
1680					if ($taskstatic->fk_task_parent != 0) {
1681						$taskstatic->fk_task_parent = $tab_conv_child_parent[$taskstatic->fk_task_parent];
1682					}
1683					$res = $taskstatic->update($user, $notrigger);
1684					if ($result_clone <= 0) {
1685						$this->error .= $taskstatic->error;
1686						$error++;
1687					}
1688				}
1689			}
1690		}
1691
1692		unset($clone_project->context['createfromclone']);
1693
1694		if (!$error) {
1695			$this->db->commit();
1696			return $clone_project_id;
1697		} else {
1698			$this->db->rollback();
1699			dol_syslog(get_class($this)."::createFromClone nbError: ".$error." error : ".$this->error, LOG_ERR);
1700			return -1;
1701		}
1702	}
1703
1704
1705	/**
1706	 *    Shift project task date from current date to delta
1707	 *
1708	 *    @param	integer		$old_project_dt_start	Old project start date
1709	 *    @return	int				                    1 if OK or < 0 if KO
1710	 */
1711	public function shiftTaskDate($old_project_dt_start)
1712	{
1713		global $user, $langs, $conf;
1714
1715		$error = 0;
1716
1717		$taskstatic = new Task($this->db);
1718
1719		// Security check
1720		$socid = 0;
1721		if ($user->socid > 0) {
1722			$socid = $user->socid;
1723		}
1724
1725		$tasksarray = $taskstatic->getTasksArray(0, 0, $this->id, $socid, 0);
1726
1727		foreach ($tasksarray as $tasktoshiftdate) {
1728			$to_update = false;
1729			// Fetch only if update of date will be made
1730			if ((!empty($tasktoshiftdate->date_start)) || (!empty($tasktoshiftdate->date_end))) {
1731				//dol_syslog(get_class($this)."::shiftTaskDate to_update", LOG_DEBUG);
1732				$to_update = true;
1733				$task = new Task($this->db);
1734				$result = $task->fetch($tasktoshiftdate->id);
1735				if (!$result) {
1736					$error++;
1737					$this->error .= $task->error;
1738				}
1739			}
1740			//print "$this->date_start + $tasktoshiftdate->date_start - $old_project_dt_start";exit;
1741
1742			//Calcultate new task start date with difference between old proj start date and origin task start date
1743			if (!empty($tasktoshiftdate->date_start)) {
1744				$task->date_start = $this->date_start + ($tasktoshiftdate->date_start - $old_project_dt_start);
1745			}
1746
1747			//Calcultate new task end date with difference between origin proj end date and origin task end date
1748			if (!empty($tasktoshiftdate->date_end)) {
1749				$task->date_end = $this->date_start + ($tasktoshiftdate->date_end - $old_project_dt_start);
1750			}
1751
1752			if ($to_update) {
1753				$result = $task->update($user);
1754				if (!$result) {
1755					$error++;
1756					$this->error .= $task->error;
1757				}
1758			}
1759		}
1760		if ($error != 0) {
1761			return -1;
1762		}
1763		return $result;
1764	}
1765
1766
1767	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1768	/**
1769	 *    Associate element to a project
1770	 *
1771	 *    @param	string	$tableName			Table of the element to update
1772	 *    @param	int		$elementSelectId	Key-rowid of the line of the element to update
1773	 *    @return	int							1 if OK or < 0 if KO
1774	 */
1775	public function update_element($tableName, $elementSelectId)
1776	{
1777		// phpcs:enable
1778		$sql = "UPDATE ".MAIN_DB_PREFIX.$tableName;
1779
1780		if ($tableName == "actioncomm") {
1781			$sql .= " SET fk_project=".$this->id;
1782			$sql .= " WHERE id=".((int) $elementSelectId);
1783		} elseif ($tableName == "entrepot") {
1784			$sql .= " SET fk_project=".$this->id;
1785			$sql .= " WHERE rowid=".((int) $elementSelectId);
1786		} else {
1787			$sql .= " SET fk_projet=".$this->id;
1788			$sql .= " WHERE rowid=".((int) $elementSelectId);
1789		}
1790
1791		dol_syslog(get_class($this)."::update_element", LOG_DEBUG);
1792		$resql = $this->db->query($sql);
1793		if (!$resql) {
1794			$this->error = $this->db->lasterror();
1795			return -1;
1796		} else {
1797			return 1;
1798		}
1799	}
1800
1801	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1802	/**
1803	 *    Associate element to a project
1804	 *
1805	 *    @param	string	$tableName			Table of the element to update
1806	 *    @param	int		$elementSelectId	Key-rowid of the line of the element to update
1807	 *    @param	string	$projectfield	    The column name that stores the link with the project
1808	 *
1809	 *    @return	int							1 if OK or < 0 if KO
1810	 */
1811	public function remove_element($tableName, $elementSelectId, $projectfield = 'fk_projet')
1812	{
1813		// phpcs:enable
1814		$sql = "UPDATE ".MAIN_DB_PREFIX.$tableName;
1815
1816		if ($tableName == "actioncomm") {
1817			$sql .= " SET fk_project=NULL";
1818			$sql .= " WHERE id=".((int) $elementSelectId);
1819		} else {
1820			$sql .= " SET ".$projectfield."=NULL";
1821			$sql .= " WHERE rowid=".((int) $elementSelectId);
1822		}
1823
1824		dol_syslog(get_class($this)."::remove_element", LOG_DEBUG);
1825		$resql = $this->db->query($sql);
1826		if (!$resql) {
1827			$this->error = $this->db->lasterror();
1828			return -1;
1829		} else {
1830			return 1;
1831		}
1832	}
1833
1834	/**
1835	 *  Create an intervention document on disk using template defined into PROJECT_ADDON_PDF
1836	 *
1837	 *  @param	string		$modele			Force template to use ('' by default)
1838	 *  @param	Translate	$outputlangs	Objet lang to use for translation
1839	 *  @param  int			$hidedetails    Hide details of lines
1840	 *  @param  int			$hidedesc       Hide description
1841	 *  @param  int			$hideref        Hide ref
1842	 *  @return int         				0 if KO, 1 if OK
1843	 */
1844	public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
1845	{
1846		global $conf, $langs;
1847
1848		$langs->load("projects");
1849
1850		if (!dol_strlen($modele)) {
1851			$modele = 'baleine';
1852
1853			if ($this->model_pdf) {
1854				$modele = $this->model_pdf;
1855			} elseif (!empty($conf->global->PROJECT_ADDON_PDF)) {
1856				$modele = $conf->global->PROJECT_ADDON_PDF;
1857			}
1858		}
1859
1860		$modelpath = "core/modules/project/doc/";
1861
1862		return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
1863	}
1864
1865
1866	/**
1867	 * Load time spent into this->weekWorkLoad and this->weekWorkLoadPerTask for all day of a week of project.
1868	 * Note: array weekWorkLoad and weekWorkLoadPerTask are reset and filled at each call.
1869	 *
1870	 * @param 	int		$datestart		First day of week (use dol_get_first_day to find this date)
1871	 * @param 	int		$taskid			Filter on a task id
1872	 * @param 	int		$userid			Time spent by a particular user
1873	 * @return 	int						<0 if OK, >0 if KO
1874	 */
1875	public function loadTimeSpent($datestart, $taskid = 0, $userid = 0)
1876	{
1877		$error = 0;
1878
1879		$this->weekWorkLoad = array();
1880		$this->weekWorkLoadPerTask = array();
1881
1882		if (empty($datestart)) {
1883			dol_print_error('', 'Error datestart parameter is empty');
1884		}
1885
1886		$sql = "SELECT ptt.rowid as taskid, ptt.task_duration, ptt.task_date, ptt.task_datehour, ptt.fk_task";
1887		$sql .= " FROM ".MAIN_DB_PREFIX."projet_task_time AS ptt, ".MAIN_DB_PREFIX."projet_task as pt";
1888		$sql .= " WHERE ptt.fk_task = pt.rowid";
1889		$sql .= " AND pt.fk_projet = ".((int) $this->id);
1890		$sql .= " AND (ptt.task_date >= '".$this->db->idate($datestart)."' ";
1891		$sql .= " AND ptt.task_date <= '".$this->db->idate(dol_time_plus_duree($datestart, 1, 'w') - 1)."')";
1892		if ($taskid) {
1893			$sql .= " AND ptt.fk_task=".((int) $taskid);
1894		}
1895		if (is_numeric($userid)) {
1896			$sql .= " AND ptt.fk_user=".((int) $userid);
1897		}
1898
1899		//print $sql;
1900		$resql = $this->db->query($sql);
1901		if ($resql) {
1902			$daylareadyfound = array();
1903
1904			$num = $this->db->num_rows($resql);
1905			$i = 0;
1906			// Loop on each record found, so each couple (project id, task id)
1907			while ($i < $num) {
1908				$obj = $this->db->fetch_object($resql);
1909				$day = $this->db->jdate($obj->task_date); // task_date is date without hours
1910				if (empty($daylareadyfound[$day])) {
1911					$this->weekWorkLoad[$day] = $obj->task_duration;
1912					$this->weekWorkLoadPerTask[$day][$obj->fk_task] = $obj->task_duration;
1913				} else {
1914					$this->weekWorkLoad[$day] += $obj->task_duration;
1915					$this->weekWorkLoadPerTask[$day][$obj->fk_task] += $obj->task_duration;
1916				}
1917				$daylareadyfound[$day] = 1;
1918				$i++;
1919			}
1920			$this->db->free($resql);
1921			return 1;
1922		} else {
1923			$this->error = "Error ".$this->db->lasterror();
1924			dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
1925			return -1;
1926		}
1927	}
1928
1929	/**
1930	 * Load time spent into this->weekWorkLoad and this->weekWorkLoadPerTask for all day of a week of project.
1931	 * Note: array weekWorkLoad and weekWorkLoadPerTask are reset and filled at each call.
1932	 *
1933	 * @param 	int		$datestart		First day of week (use dol_get_first_day to find this date)
1934	 * @param 	int		$taskid			Filter on a task id
1935	 * @param 	int		$userid			Time spent by a particular user
1936	 * @return 	int						<0 if OK, >0 if KO
1937	 */
1938	public function loadTimeSpentMonth($datestart, $taskid = 0, $userid = 0)
1939	{
1940		$error = 0;
1941
1942		$this->monthWorkLoad = array();
1943		$this->monthWorkLoadPerTask = array();
1944
1945		if (empty($datestart)) {
1946			dol_print_error('', 'Error datestart parameter is empty');
1947		}
1948
1949		$sql = "SELECT ptt.rowid as taskid, ptt.task_duration, ptt.task_date, ptt.task_datehour, ptt.fk_task";
1950		$sql .= " FROM ".MAIN_DB_PREFIX."projet_task_time AS ptt, ".MAIN_DB_PREFIX."projet_task as pt";
1951		$sql .= " WHERE ptt.fk_task = pt.rowid";
1952		$sql .= " AND pt.fk_projet = ".((int) $this->id);
1953		$sql .= " AND (ptt.task_date >= '".$this->db->idate($datestart)."' ";
1954		$sql .= " AND ptt.task_date <= '".$this->db->idate(dol_time_plus_duree($datestart, 1, 'm') - 1)."')";
1955		if ($task_id) {
1956			$sql .= " AND ptt.fk_task=".((int) $taskid);
1957		}
1958		if (is_numeric($userid)) {
1959			$sql .= " AND ptt.fk_user=".((int) $userid);
1960		}
1961
1962		//print $sql;
1963		$resql = $this->db->query($sql);
1964		if ($resql) {
1965			$weekalreadyfound = array();
1966
1967			$num = $this->db->num_rows($resql);
1968			$i = 0;
1969			// Loop on each record found, so each couple (project id, task id)
1970			while ($i < $num) {
1971				$obj = $this->db->fetch_object($resql);
1972				if (!empty($obj->task_date)) {
1973					$date = explode('-', $obj->task_date);
1974					$week_number = getWeekNumber($date[2], $date[1], $date[0]);
1975				}
1976				if (empty($weekalreadyfound[$week_number])) {
1977					$this->monthWorkLoad[$week_number] = $obj->task_duration;
1978					$this->monthWorkLoadPerTask[$week_number][$obj->fk_task] = $obj->task_duration;
1979				} else {
1980					$this->monthWorkLoad[$week_number] += $obj->task_duration;
1981					$this->monthWorkLoadPerTask[$week_number][$obj->fk_task] += $obj->task_duration;
1982				}
1983				$weekalreadyfound[$week_number] = 1;
1984				$i++;
1985			}
1986			$this->db->free($resql);
1987			return 1;
1988		} else {
1989			$this->error = "Error ".$this->db->lasterror();
1990			dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
1991			return -1;
1992		}
1993	}
1994
1995
1996	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1997	/**
1998	 * Load indicators for dashboard (this->nbtodo and this->nbtodolate)
1999	 *
2000	 * @param	User	$user   Objet user
2001	 * @return WorkboardResponse|int <0 if KO, WorkboardResponse if OK
2002	 */
2003	public function load_board($user)
2004	{
2005		// phpcs:enable
2006		global $conf, $langs;
2007
2008		// For external user, no check is done on company because readability is managed by public status of project and assignement.
2009		//$socid=$user->socid;
2010
2011		$projectsListId = null;
2012		if (!$user->rights->projet->all->lire) {
2013			$projectsListId = $this->getProjectsAuthorizedForUser($user, 0, 1);
2014		}
2015
2016		$sql = "SELECT p.rowid, p.fk_statut as status, p.fk_opp_status, p.datee as datee";
2017		$sql .= " FROM (".MAIN_DB_PREFIX."projet as p";
2018		$sql .= ")";
2019		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s on p.fk_soc = s.rowid";
2020		// For external user, no check is done on company permission because readability is managed by public status of project and assignement.
2021		//if (! $user->rights->societe->client->voir && ! $socid) $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe_commerciaux as sc ON sc.fk_soc = s.rowid";
2022		$sql .= " WHERE p.fk_statut = 1";
2023		$sql .= " AND p.entity IN (".getEntity('project').')';
2024		if (!empty($projectsListId)) {
2025			$sql .= " AND p.rowid IN (".$this->db->sanitize($projectsListId).")";
2026		}
2027		// No need to check company, as filtering of projects must be done by getProjectsAuthorizedForUser
2028		//if ($socid || ! $user->rights->societe->client->voir)	$sql.= "  AND (p.fk_soc IS NULL OR p.fk_soc = 0 OR p.fk_soc = ".((int) $socid).")";
2029		// For external user, no check is done on company permission because readability is managed by public status of project and assignement.
2030		//if (! $user->rights->societe->client->voir && ! $socid) $sql.= " AND ((s.rowid = sc.fk_soc AND sc.fk_user = ".((int) $user->id).") OR (s.rowid IS NULL))";
2031
2032		//print $sql;
2033		$resql = $this->db->query($sql);
2034		if ($resql) {
2035			$project_static = new Project($this->db);
2036
2037			$response = new WorkboardResponse();
2038			$response->warning_delay = $conf->projet->warning_delay / 60 / 60 / 24;
2039			$response->label = $langs->trans("OpenedProjects");
2040			$response->labelShort = $langs->trans("Opened");
2041			if ($user->rights->projet->all->lire) {
2042				$response->url = DOL_URL_ROOT.'/projet/list.php?search_status=1&mainmenu=project';
2043			} else {
2044				$response->url = DOL_URL_ROOT.'/projet/list.php?search_project_user=-1&search_status=1&mainmenu=project';
2045			}
2046			$response->img = img_object('', "projectpub");
2047
2048			// This assignment in condition is not a bug. It allows walking the results.
2049			while ($obj = $this->db->fetch_object($resql)) {
2050				$response->nbtodo++;
2051
2052				$project_static->statut = $obj->status;
2053				$project_static->opp_status = $obj->fk_opp_status;
2054				$project_static->datee = $this->db->jdate($obj->datee);
2055
2056				if ($project_static->hasDelay()) {
2057					$response->nbtodolate++;
2058				}
2059			}
2060
2061			return $response;
2062		} else {
2063			$this->error = $this->db->error();
2064			return -1;
2065		}
2066	}
2067
2068
2069	/**
2070	 * Function used to replace a thirdparty id with another one.
2071	 *
2072	 * @param DoliDB $db Database handler
2073	 * @param int $origin_id Old thirdparty id
2074	 * @param int $dest_id New thirdparty id
2075	 * @return bool
2076	 */
2077	public static function replaceThirdparty(DoliDB $db, $origin_id, $dest_id)
2078	{
2079		$tables = array(
2080			'projet'
2081		);
2082
2083		return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
2084	}
2085
2086
2087	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2088	/**
2089	 * Charge indicateurs this->nb pour le tableau de bord
2090	 *
2091	 * @return     int         <0 if KO, >0 if OK
2092	 */
2093	public function load_state_board()
2094	{
2095		// phpcs:enable
2096		global $user;
2097
2098		$this->nb = array();
2099
2100		$sql = "SELECT count(p.rowid) as nb";
2101		$sql .= " FROM ".MAIN_DB_PREFIX."projet as p";
2102		$sql .= " WHERE";
2103		$sql .= " p.entity IN (".getEntity('project').")";
2104		if (!$user->rights->projet->all->lire) {
2105			$projectsListId = $this->getProjectsAuthorizedForUser($user, 0, 1);
2106			$sql .= "AND p.rowid IN (".$this->db->sanitize($projectsListId).")";
2107		}
2108
2109		$resql = $this->db->query($sql);
2110		if ($resql) {
2111			while ($obj = $this->db->fetch_object($resql)) {
2112				$this->nb["projects"] = $obj->nb;
2113			}
2114			$this->db->free($resql);
2115			return 1;
2116		} else {
2117			dol_print_error($this->db);
2118			$this->error = $this->db->error();
2119			return -1;
2120		}
2121	}
2122
2123
2124	/**
2125	 * Is the project delayed?
2126	 *
2127	 * @return bool
2128	 */
2129	public function hasDelay()
2130	{
2131		global $conf;
2132
2133		if (!($this->statut == self::STATUS_VALIDATED)) {
2134			return false;
2135		}
2136		if (!$this->datee && !$this->date_end) {
2137			return false;
2138		}
2139
2140		$now = dol_now();
2141
2142		return ($this->datee ? $this->datee : $this->date_end) < ($now - $conf->projet->warning_delay);
2143	}
2144
2145
2146	/**
2147	 *	Charge les informations d'ordre info dans l'objet commande
2148	 *
2149	 *	@param  int		$id       Id of order
2150	 *	@return	void
2151	 */
2152	public function info($id)
2153	{
2154		$sql = 'SELECT c.rowid, datec as datec, tms as datem,';
2155		$sql .= ' date_close as datecloture,';
2156		$sql .= ' fk_user_creat as fk_user_author, fk_user_close as fk_use_cloture';
2157		$sql .= ' FROM '.MAIN_DB_PREFIX.'projet as c';
2158		$sql .= ' WHERE c.rowid = '.((int) $id);
2159		$result = $this->db->query($sql);
2160		if ($result) {
2161			if ($this->db->num_rows($result)) {
2162				$obj = $this->db->fetch_object($result);
2163				$this->id = $obj->rowid;
2164				if ($obj->fk_user_author) {
2165					$cuser = new User($this->db);
2166					$cuser->fetch($obj->fk_user_author);
2167					$this->user_creation = $cuser;
2168				}
2169
2170				if ($obj->fk_user_cloture) {
2171					$cluser = new User($this->db);
2172					$cluser->fetch($obj->fk_user_cloture);
2173					$this->user_cloture = $cluser;
2174				}
2175
2176				$this->date_creation     = $this->db->jdate($obj->datec);
2177				$this->date_modification = $this->db->jdate($obj->datem);
2178				$this->date_cloture      = $this->db->jdate($obj->datecloture);
2179			}
2180
2181			$this->db->free($result);
2182		} else {
2183			dol_print_error($this->db);
2184		}
2185	}
2186
2187	/**
2188	 * Sets object to supplied categories.
2189	 *
2190	 * Deletes object from existing categories not supplied.
2191	 * Adds it to non existing supplied categories.
2192	 * Existing categories are left untouch.
2193	 *
2194	 * @param int[]|int $categories Category or categories IDs
2195	 * @return void
2196	 */
2197	public function setCategories($categories)
2198	{
2199		require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
2200		return parent::setCategoriesCommon($categories, Categorie::TYPE_PROJECT);
2201	}
2202
2203
2204	/**
2205	 * 	Create an array of tasks of current project
2206	 *
2207	 *  @param  User   $user       Object user we want project allowed to
2208	 * 	@return int		           >0 if OK, <0 if KO
2209	 */
2210	public function getLinesArray($user)
2211	{
2212		require_once DOL_DOCUMENT_ROOT.'/projet/class/task.class.php';
2213		$taskstatic = new Task($this->db);
2214
2215		$this->lines = $taskstatic->getTasksArray(0, $user, $this->id, 0, 0);
2216	}
2217}
2218