<?php
/* Copyright (C) 2013-2018  Jean-François Ferry 	<hello@librethic.io>
 * Copyright (C) 2016       Christophe Battarel 	<christophe@altairis.fr>
 * Copyright (C) 2019-2025  Frédéric France         <frederic.france@free.fr>
 * Copyright (C) 2020       Laurent Destailleur 	<eldy@users.sourceforge.net>
 * Copyright (C) 2023-2025  Charlene Benke 	   		<charlene@patas-monkey.com>
 * Copyright (C) 2023-2024  Benjamin Falière	    <benjamin.faliere@altairis.fr>
 * Copyright (C) 2024		William Mead			<william.mead@manchenumerique.fr>
 * Copyright (C) 2024-2025	MDW						<mdeweerd@users.noreply.github.com>
 * Copyright (C) 2026		Jon Bendtsen          	<jon.bendtsen.github@jonb.dk>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

/**
 *  \file       ticket/class/ticket.class.php
 *  \ingroup    ticket
 *  \brief      Class file for object ticket
 */

// Put here all includes required by your class file
require_once DOL_DOCUMENT_ROOT."/core/class/commonobject.class.php";
require_once DOL_DOCUMENT_ROOT.'/fichinter/class/fichinter.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/ticket.lib.php';


/**
 *    Class to manage ticket
 */
class Ticket extends CommonObject
{
	/**
	 * @var DoliDB Database handler
	 */
	public $db;

	/**
	 * @var string ID to identify managed object
	 */
	public $element = 'ticket';

	/**
	 * @var string Name of table without prefix where object is stored
	 */
	public $table_element = 'ticket';

	/**
	 * @var string Name of field for link to tickets
	 */
	public $fk_element = 'fk_ticket';

	/**
	 * @var string String with name of icon for ticketcore. Must be the part after the 'object_' into object_ticketcore.png
	 */
	public $picto = 'ticket';

	/**
	 * @var ?string Hash to identify ticket publicly
	 */
	public $track_id;

	/**
	 * @var int Third party ID
	 */
	public $fk_soc;

	/**
	 * @var int Third party ID
	 */
	public $socid;

	/**
	 * @var int Contract ID
	 */
	public $fk_contract;

	/**
	 * @var ?string Email of person who created the ticket
	 */
	public $origin_email;

	/**
	 * @var int User id who created the ticket
	 */
	public $fk_user_create;

	/**
	 * @var int User id who the ticket is assigned to
	 */
	public $fk_user_assign;

	/**
	 * @var string Ticket subject
	 */
	public $subject;

	/**
	 * @var string Ticket message
	 */
	public $message;

	/**
	 * @var string If message is private
	 */
	public $private;

	/**
	 * @var ?int Ticket status
	 * @deprecated use status
	 * @see $status
	 */
	public $fk_statut;

	/**
	 * @var ?int Ticket status
	 */
	public $status;

	/**
	 * @var ?int State resolution
	 */
	public $resolution;

	/**
	 * @var ?int Progress in percent
	 */
	public $progress;

	/**
	 * @var string Duration for ticket
	 */
	public $timing;

	/**
	 * @var string Type code
	 */
	public $type_code;

	/**
	 * @var string Category code
	 */
	public $category_code;

	/**
	 * @var string Severity code
	 */
	public $severity_code;

	/**
	 * @var string Type label
	 */
	public $type_label;

	/**
	 * @var string Category label
	 */
	public $category_label;

	/**
	 * @var string Severity label
	 */
	public $severity_label;

	/**
	 * @var string Email from user
	 */
	public $email_from;

	/**
	 * @var string Email to reply to
	 */
	public $origin_replyto;

	/**
	 * @var string References from origin email
	 */
	public $origin_references;

	/**
	 * @var int Creation date
	 */
	public $datec;

	/**
	 * @var int Read date
	 */
	public $date_read;

	/**
	 * @var int Last message date
	 */
	public $date_last_msg_sent;

	/**
	 * @var int 	Close ticket date
	 */
	public $date_close;

	/**
	 * @var array<int,array{code:string,label:string,use_default:int,pos:int}> cache_types_tickets
	 */
	public $cache_types_tickets;

	/**
	 * @var array<int,array{id:int,private:int,fk_user_author:int,fk_user_action:int,fk_contact_author?:string,datec:int,datep:int,subject:string,message:string}> cache msgs ticket  // fk_contact_author is email, not key in the cache
	 */
	public $cache_msgs_ticket;

	/**
	 * @var int 	Save if a thirdparty was notified at creation at ticket or not
	 */
	public $notify_tiers_at_create;

	/**
	 * @var string 	Email MSGID
	 */
	public $email_msgid;

	/**
	 * @var null|int|'' 	Email Date
	 */
	public $email_date;

	/**
	 * @var string 			IP address
	 */
	public $ip;

	/**
	 * @var Ticket[] 		Array of Tickets
	 */
	public $lines;

	/**
	 * @var string Regex pour les images
	 */
	public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php

	/**
	 * Status
	 */
	const STATUS_NOT_READ = 0;			// Draft. Not take into account yet.
	const STATUS_READ = 1;				// Ticket was read.
	const STATUS_ASSIGNED = 2;			// Ticket was just assigned to someone. Not in progress yet.
	const STATUS_IN_PROGRESS = 3;		// In progress
	const STATUS_NEED_MORE_INFO = 5;	// Waiting requester feedback
	const STATUS_WAITING = 7;			// On hold
	const STATUS_CLOSED = 8;			// Closed - Solved
	const STATUS_CANCELED = 9;			// Closed - Not solved


	/**
	 *  'type' field format ('integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter]]', 'sellist:TableName:LabelFieldName[:KeyFieldName[:KeyFieldParent[:Filter]]]', 'varchar(x)', 'double(24,8)', 'real', 'price', 'text', 'text:none', 'html', 'date', 'datetime', 'timestamp', 'duration', 'mail', 'phone', 'url', 'password')
	 *         Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:>:'20160101') or (t.nature:is:NULL)"
	 *  'label' the translation key.
	 *  'picto' is code of a picto to show before value in forms
	 *  'enabled' is a condition when the field must be managed (Example: 1 or 'getDolGlobalString('MY_SETUP_PARAM'))
	 *  'position' is the sort order of field.
	 *  'notnull' is set to 1 if not null in database. Set to -1 if we must set data to null if empty ('' or 0).
	 *  '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)
	 *  'noteditable' says if field is not editable (1 or 0)
	 *  'default' is a default value for creation (can still be overwritten by the Setup of Default Values if the 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.
	 *  'index' if we want an index in database.
	 *  'foreignkey'=>'tablename.field' if the field is a foreign key (it is recommended to name the field fk_...).
	 *  'searchall' is 1 if we want to search in this field when making a search from the quick search button.
	 *  '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).
	 *  'css' and 'cssview' and 'csslist' is the CSS style to use on field. 'css' is used in creation and update. 'cssview' is used in view mode. 'csslist' is used for columns in lists. For example: 'maxwidth200', 'wordbreak', 'tdoverflowmax200'
	 *  'help' is a 'TranslationString' to use to show a tooltip on field. You can also use 'TranslationString:keyfortooltiponlick' for a tooltip on click.
	 *  'showoncombobox' if value of the field must be visible into the label of the combobox that list record
	 *  '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.
	 *  'arrayofkeyval' to set list of value if type is a list of predefined values. For example: array("0"=>"Draft","1"=>"Active","-1"=>"Cancel")
	 *  'autofocusoncreate' to have field having the focus on a create form. Only 1 field should have this property set to 1.
	 *  'comment' is not used. You can store here any text of your choice. It is not used by application.
	 *
	 *  Note: To have value dynamic, you can set value to 0 in definition and edit the value on the fly into the constructor.
	 */

	// BEGIN MODULEBUILDER PROPERTIES
	public $fields = array(
		'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'visible' => -2, 'enabled' => 1, 'position' => 1, 'notnull' => 1, 'index' => 1, 'comment' => "Id"),
		'entity' => array('type' => 'integer', 'label' => 'Entity', 'visible' => 0, 'enabled' => 1, 'position' => 5, 'notnull' => 1, 'index' => 1),
		'ref' => array('type' => 'varchar(128)', 'label' => 'Ref', 'visible' => 1, 'enabled' => 1, 'position' => 10, 'notnull' => 1, 'index' => 1, 'searchall' => 1, 'comment' => "Reference of object", 'css' => '', 'showoncombobox' => 1),
		'track_id' => array('type' => 'varchar(255)', 'label' => 'TicketTrackId', 'visible' => -2, 'enabled' => 1, 'position' => 11, 'notnull' => -1, 'searchall' => 1, 'help' => "Help text"),
		'fk_user_create' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'Author', 'visible' => 1, 'enabled' => 1, 'position' => 15, 'notnull' => 1, 'csslist' => 'tdoverflowmax100 maxwidth150onsmartphone'),
		'origin_email' => array('type' => 'mail', 'label' => 'OriginEmail', 'visible' => -2, 'enabled' => 1, 'position' => 16, 'notnull' => 1, 'index' => 1, 'searchall' => 1, 'comment' => "Reference of object", 'csslist' => 'tdoverflowmax150'),
		'origin_replyto' => array('type' => 'mail', 'label' => 'EmailReplyto', 'visible' => -2, 'enabled' => 1, 'position' => 17, 'notnull' => 1, 'index' => 1, 'searchall' => 1, 'comment' => "Email to reply to", 'csslist' => 'tdoverflowmax150'),
		'origin_references' => array('type' => 'text', 'label' => 'EmailReferences', 'visible' => -2, 'enabled' => 1, 'position' => 18, 'notnull' => 1, 'index' => 1, 'searchall' => 1, 'comment' => "References from origin email", 'csslist' => 'tdoverflowmax150'),
		'subject' => array('type' => 'varchar(255)', 'label' => 'Subject', 'visible' => 1, 'enabled' => 1, 'position' => 19, 'notnull' => -1, 'searchall' => 1, 'help' => "", 'css' => 'maxwidth200 tdoverflowmax200', 'csslist' => 'tdoverflowmax250', 'autofocusoncreate' => 1),
		'type_code' => array('type' => 'varchar(32)', 'label' => 'Type', 'visible' => 1, 'enabled' => 1, 'position' => 20, 'notnull' => -1, 'help' => "", 'csslist' => 'tdoverflowmax100'),
		'category_code' => array('type' => 'varchar(32)', 'label' => 'TicketCategory', 'visible' => -1, 'enabled' => 1, 'position' => 21, 'notnull' => -1, 'help' => "", 'css' => 'maxwidth100 tdoverflowmax200'),
		'severity_code' => array('type' => 'varchar(32)', 'label' => 'Severity', 'visible' => 1, 'enabled' => 1, 'position' => 22, 'notnull' => -1, 'help' => "", 'css' => 'maxwidth100 tdoverflowmax100'),
		'fk_soc' => array('type' => 'integer:Societe:societe/class/societe.class.php', 'label' => 'ThirdParty', 'visible' => 1, 'enabled' => 'isModEnabled("societe")', 'position' => 50, 'notnull' => -1, 'index' => 1, 'searchall' => 1, 'help' => "OrganizationEventLinkToThirdParty", 'css' => 'tdoverflowmax150 maxwidth150onsmartphone'),
		'notify_tiers_at_create' => array('type' => 'integer', 'label' => 'NotifyThirdparty', 'visible' => -1, 'enabled' => 0, 'position' => 51, 'notnull' => 1, 'index' => 1),
		'fk_project' => array('type' => 'integer:Project:projet/class/project.class.php', 'label' => 'Project', 'visible' => -1, 'enabled' => 'isModEnabled("project")', 'position' => 52, 'notnull' => -1, 'index' => 1, 'help' => "LinkToProject"),
		'fk_contract' => array('type' => 'integer:Contrat:contrat/class/contrat.class.php', 'label' => 'Contract', 'visible' => -1, 'enabled' => 'isModEnabled("contract")', 'position' => 53, 'notnull' => -1, 'index' => 1, 'help' => "LinkToContract"),
		//'timing' => array('type'=>'varchar(20)', 'label'=>'Timing', 'visible'=>-1, 'enabled'=>1, 'position'=>42, 'notnull'=>-1, 'help'=>""),	// what is this ?
		'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'visible' => 1, 'enabled' => 1, 'position' => 500, 'notnull' => 1, 'csslist' => 'nowraponall'),
		'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'visible' => -1, 'enabled' => 1, 'position' => 501, 'notnull' => 1),
		'date_read' => array('type' => 'datetime', 'label' => 'DateReading', 'visible' => -1, 'enabled' => 1, 'position' => 505, 'notnull' => 1, 'csslist' => 'nowraponall'),
		'date_last_msg_sent' => array('type' => 'datetime', 'label' => 'TicketLastMessageDate', 'visible' => 0, 'enabled' => 1, 'position' => 506, 'notnull' => -1),
		'fk_user_assign' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'AssignedTo', 'visible' => 1, 'enabled' => 1, 'position' => 507, 'notnull' => 1, 'csslist' => 'tdoverflowmax100 maxwidth150onsmartphone'),
		'date_close' => array('type' => 'datetime', 'label' => 'TicketCloseOn', 'visible' => -1, 'enabled' => 1, 'position' => 510, 'notnull' => 1),
		'message' => array('type' => 'html', 'label' => 'Message', 'visible' => -2, 'enabled' => 1, 'position' => 540, 'notnull' => -1,),
		'note_private' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 110),
		'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 115),
		'email_msgid' => array('type' => 'varchar(255)', 'label' => 'EmailMsgID', 'visible' => -2, 'enabled' => 1, 'position' => 540, 'notnull' => -1, 'help' => 'EmailMsgIDDesc', 'csslist' => 'tdoverflowmax100'),
		'email_date' => array('type' => 'datetime', 'label' => 'EmailDate', 'visible' => -2, 'enabled' => 1, 'position' => 541),
		'progress' => array('type' => 'integer', 'label' => 'Progression', 'visible' => -1, 'enabled' => 1, 'position' => 540, 'notnull' => -1, 'css' => 'right', 'help' => "", 'isameasure' => 1, 'csslist' => 'width50'),
		'resolution' => array('type' => 'integer', 'label' => 'Resolution', 'visible' => -1, 'enabled' => 'getDolGlobalString("TICKET_ENABLE_RESOLUTION")', 'position' => 550, 'notnull' => 1),
		'model_pdf' => array('type' => 'varchar(255)', 'label' => 'PDFTemplate', 'enabled' => 1, 'visible' => 0, 'position' => 560),
		'extraparams' => array('type' => 'varchar(255)', 'label' => 'Extraparams', 'enabled' => 1, 'visible' => 0, 'position' => 570),
		'fk_statut' => array('type' => 'integer', 'label' => 'Status', 'visible' => 1, 'enabled' => 1, 'position' => 600, 'notnull' => 1, 'index' => 1, 'arrayofkeyval' => array(0 => 'Unread', 1 => 'Read', 2 => 'Assigned', 3 => 'InProgress', 5 => 'NeedMoreInformation', 7 => 'OnHold', 8 => 'SolvedClosed', 9 => 'Deleted')),
		'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'position' => 900),
	);
	// END MODULEBUILDER PROPERTIES


	/**
	 *  Constructor
	 *
	 *  @param DoliDB $db Database handler
	 */
	public function __construct(DoliDB $db)
	{
		$this->db = $db;

		$this->ismultientitymanaged = 1;
		$this->isextrafieldmanaged = 1;

		$this->labelStatusShort = array(
			self::STATUS_NOT_READ => 'Unread',
			self::STATUS_READ => 'Read',
			self::STATUS_ASSIGNED => 'Assigned',
			self::STATUS_IN_PROGRESS => 'InProgress',
			self::STATUS_NEED_MORE_INFO => 'NeedMoreInformationShort',
			self::STATUS_WAITING => 'OnHold',
			self::STATUS_CLOSED => 'SolvedClosed',
			self::STATUS_CANCELED => 'Canceled'
		);
		$this->labelStatus = array(
			self::STATUS_NOT_READ => 'Unread',
			self::STATUS_READ => 'Read',
			self::STATUS_ASSIGNED => 'Assigned',
			self::STATUS_IN_PROGRESS => 'InProgress',
			self::STATUS_NEED_MORE_INFO => 'NeedMoreInformation',
			self::STATUS_WAITING => 'OnHold',
			self::STATUS_CLOSED => 'SolvedClosed',
			self::STATUS_CANCELED => 'Canceled'
		);

		if (!getDolGlobalString('TICKET_INCLUDE_SUSPENDED_STATUS')) {
			unset($this->fields['fk_statut']['arrayofkeyval'][self::STATUS_WAITING]);
			unset($this->labelStatusShort[self::STATUS_WAITING]);
			unset($this->labelStatus[self::STATUS_WAITING]);
		}
	}

	/**
	 *    Check properties of ticket are ok (like ref, track_id, ...).
	 *    All properties must be already loaded on object (this->ref, this->track_id, ...).
	 *
	 *    @return int        0 if OK, <0 if KO
	 */
	private function verify()
	{
		$this->errors = array();

		$result = 0;

		// Clean parameters
		if (isset($this->ref)) {
			$this->ref = trim($this->ref);
		}

		if (isset($this->track_id)) {
			$this->track_id = trim($this->track_id);
		}

		if (isset($this->fk_soc)) {
			$this->fk_soc = (int) $this->fk_soc;
		}

		if (isset($this->fk_project)) {
			$this->fk_project = (int) $this->fk_project;
		}

		if (isset($this->origin_email)) {
			$this->origin_email = trim($this->origin_email);
		}

		if (isset($this->fk_user_create)) {
			$this->fk_user_create = (int) $this->fk_user_create;
		}

		if (isset($this->fk_user_assign)) {
			$this->fk_user_assign = (int) $this->fk_user_assign;
		}

		if (isset($this->subject)) {
			$this->subject = trim($this->subject);
		}

		if (isset($this->message)) {
			$this->message = trim($this->message);
			if (dol_strlen($this->message) > 65000) {
				global $langs;
				$langs->loadLangs(array('errors', 'ticket'));
				$this->errors[] = $langs->trans('ErrorFieldTooLong', $langs->transnoentitiesnoconv('InitialMessage'));
				dol_syslog(get_class($this).'::create error -1 message too long', LOG_ERR);
				$result = -1;
			}
		}

		if (isset($this->status)) {
			$this->status = (int) $this->status;
		}

		if (isset($this->resolution)) {
			$this->resolution = (int) $this->resolution;
		}

		if (isset($this->progress)) {
			$this->progress = (int) $this->progress;
		}

		if (isset($this->timing)) {
			$this->timing = trim($this->timing);
		}

		if (isset($this->type_code)) {
			$this->type_code = trim($this->type_code);
		}

		if (isset($this->category_code)) {
			$this->category_code = trim($this->category_code);
		}

		if (isset($this->severity_code)) {
			$this->severity_code = trim($this->severity_code);
		}

		if (empty($this->ref)) {
			$this->errors[] = 'ErrorTicketRefRequired';
			dol_syslog(get_class($this)."::create error -1 ref null", LOG_ERR);
			$result = -1;
		}

		return $result;
	}

	/**
	 * Check if ref exists or not
	 *
	 * @param string $action    Action
	 * @param string $getRef    Reference of object
	 * @return bool
	 */
	public function checkExistingRef(string $action, string $getRef)
	{
		$test = new self($this->db);

		if ($test->fetch(0, $getRef) > 0) {
			if (($action == 'add') || ($action == 'update' && $this->ref != $getRef)) {
				return true;
			}
		}

		$this->ref = $getRef;
		return false;
	}

	/**
	 *  Create object into database
	 *
	 *  @param  User $user      User that creates
	 *  @param  int  $notrigger 0=launch triggers after, 1=disable triggers
	 *  @return int                      Return integer <0 if KO, Id of created object if OK
	 */
	public function create($user, $notrigger = 0)
	{
		global $conf;

		$error = 0;

		// Clean parameters
		if (empty($this->datec)) {
			$this->datec = dol_now();
		}
		if (empty($this->track_id)) {
			$this->track_id = generate_random_id(16);
		}

		// Check more parameters
		// If error, this->errors[] is filled
		$result = $this->verify();

		if ($result >= 0) {
			// setEntity will set entity with the right value if empty or change it for the right value if multicompany module is active
			$this->entity = setEntity($this);

			// Insert request
			$sql = "INSERT INTO ".MAIN_DB_PREFIX."ticket(";
			$sql .= "ref,";
			$sql .= "track_id,";
			$sql .= "fk_soc,";
			$sql .= "fk_project,";
			$sql .= "fk_contract,";
			$sql .= "origin_email,";
			$sql .= "origin_replyto,";
			$sql .= "origin_references,";
			$sql .= "fk_user_create,";
			$sql .= "fk_user_assign,";
			$sql .= "email_msgid,";
			$sql .= "email_date,";
			$sql .= "subject,";
			$sql .= "message,";
			$sql .= "note_private,";
			$sql .= "note_public,";
			$sql .= "fk_statut,";
			$sql .= "resolution,";
			$sql .= "progress,";
			$sql .= "timing,";
			$sql .= "type_code,";
			$sql .= "category_code,";
			$sql .= "severity_code,";
			$sql .= "datec,";
			$sql .= "date_read,";
			$sql .= "date_close,";
			$sql .= "entity,";
			$sql .= "notify_tiers_at_create,";
			$sql .= "model_pdf,";
			$sql .= "ip";
			$sql .= ") VALUES (";
			$sql .= " ".(!isset($this->ref) ? '' : "'".$this->db->escape($this->ref)."'").",";
			$sql .= " ".(!isset($this->track_id) ? 'NULL' : "'".$this->db->escape($this->track_id)."'").",";
			$sql .= " ".($this->fk_soc > 0 ? ((int) $this->fk_soc) : "null").",";
			$sql .= " ".($this->fk_project > 0 ? ((int) $this->fk_project) : "null").",";
			$sql .= " ".($this->fk_contract > 0 ? ((int) $this->fk_contract) : "null").",";
			$sql .= " ".(!isset($this->origin_email) ? 'NULL' : "'".$this->db->escape($this->origin_email)."'").",";
			$sql .= " ".(!isset($this->origin_replyto) ? 'NULL' : "'".$this->db->escape($this->origin_replyto)."'").",";
			$sql .= " ".(!isset($this->origin_references) ? 'NULL' : "'".$this->db->escape($this->origin_references)."'").",";
			$sql .= " ".(!isset($this->fk_user_create) ? ($user->id > 0 ? ((int) $user->id) : 'NULL') : ($this->fk_user_create > 0 ? ((int) $this->fk_user_create) : 'NULL')).",";
			$sql .= " ".($this->fk_user_assign > 0 ? ((int) $this->fk_user_assign) : 'NULL').",";
			$sql .= " ".(empty($this->email_msgid) ? 'NULL' : "'".$this->db->escape($this->email_msgid)."'").",";
			$sql .= " ".(!isDolTms($this->email_date) ? 'NULL' : "'".$this->db->idate($this->email_date)."'").",";
			$sql .= " ".(!isset($this->subject) ? 'NULL' : "'".$this->db->escape($this->subject)."'").",";
			$sql .= " ".(!isset($this->message) ? 'NULL' : "'".$this->db->escape($this->message)."'").",";
			$sql .= " ".(!isset($this->note_private) ? 'NULL' : "'".$this->db->escape($this->note_private)."'").",";
			$sql .= " ".(!isset($this->note_public) ? 'NULL' : "'".$this->db->escape($this->note_public)."'").",";
			$sql .= " ".(!isset($this->status) ? '0' : ((int) $this->status)).",";
			$sql .= " ".(!isset($this->resolution) ? 'NULL' : ((int) $this->resolution)).",";
			$sql .= " ".(!isset($this->progress) ? '0' : ((int) $this->progress)).",";
			$sql .= " ".(!isset($this->timing) ? 'NULL' : "'".$this->db->escape($this->timing)."'").",";
			$sql .= " ".(!isset($this->type_code) ? 'NULL' : "'".$this->db->escape($this->type_code)."'").",";
			$sql .= " ".(empty($this->category_code) || $this->category_code == '-1' ? 'NULL' : "'".$this->db->escape($this->category_code)."'").",";
			$sql .= " ".(!isset($this->severity_code) ? 'NULL' : "'".$this->db->escape($this->severity_code)."'").",";
			$sql .= " ".(!isDolTms($this->datec) ? 'NULL' : "'".$this->db->idate($this->datec)."'").",";
			$sql .= " ".(!isset($this->date_read) || dol_strlen((string) $this->date_read) == 0 ? 'NULL' : "'".$this->db->idate($this->date_read)."'").",";
			$sql .= " ".(!isset($this->date_close) || dol_strlen((string) $this->date_close) == 0 ? 'NULL' : "'".$this->db->idate($this->date_close)."'");
			$sql .= ", ".((int) $this->entity);
			$sql .= ", ".(!isset($this->notify_tiers_at_create) ? 1 : ((int) $this->notify_tiers_at_create));
			$sql .= ", '".$this->db->escape($this->model_pdf)."'";
			$sql .= ", ".(!isset($this->ip) ? 'NULL' : "'".$this->db->escape($this->ip)."'");
			$sql .= ")";

			$this->db->begin();

			dol_syslog(get_class($this)."::create", LOG_DEBUG);
			$resql = $this->db->query($sql);
			if (!$resql) {
				$error++;
				$this->errors[] = "Error ".$this->db->lasterror();
			}

			if (!$error) {
				$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."ticket");
			}

			if (!$error && getDolGlobalString('TICKET_ADD_AUTHOR_AS_CONTACT') && empty($this->context["createdfrompublicinterface"])) {
				// add creator as contributor

				// We first check the type of contact (internal or external)
				if (!empty($user->socid) && !empty($user->contact_id) && getDolGlobalInt('TICKET_ADD_AUTHOR_AS_CONTACT') == 2) {
					$contact_type = 'external';
					$contributor_id = $user->contact_id;
				} else {
					$contact_type = 'internal';
					$contributor_id = $user->id;
				}

				// We add the creator as contributor
				if ($this->add_contact($contributor_id, 'CONTRIBUTOR', $contact_type) < 0) {
					$error++;
				}
			}

			if (!$error && $this->fk_user_assign > 0) {
				if ($this->add_contact($this->fk_user_assign, 'SUPPORTTEC', 'internal') < 0) {
					$error++;
				}
			}


			//Update extrafield
			if (!$error) {
				$result = $this->insertExtraFields();
				if ($result < 0) {
					$error++;
				}
			}

			if (!$error && !$notrigger) {
				// Call trigger
				$result = $this->call_trigger('TICKET_CREATE', $user);
				if ($result < 0) {
					$error++;
				}
				// End call triggers
			}

			// Commit or rollback
			if ($error) {
				foreach ($this->errors as $errmsg) {
					dol_syslog(get_class($this)."::create ".$errmsg, LOG_ERR);
					$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
				}
				$this->db->rollback();
				return -1 * $error;
			} else {
				$this->db->commit();
				return $this->id;
			}
		} else {
			$this->db->rollback();
			dol_syslog(get_class($this)."::Create fails verify ".implode(',', $this->errors), LOG_WARNING);
			return -3;
		}
	}

	/**
	 *  Load object in memory from the database
	 *
	 *  @param  int        	$id    			Id object
	 *  @param	string		$ref			Ref
	 *  @param	string		$track_id		Track id, a hash like ref
	 *  @param	string		$email_msgid	Email msgid
	 *  @return int              			Return integer <0 if KO, >0 if OK
	 */
	public function fetch($id = 0, $ref = '', $track_id = '', $email_msgid = '')
	{
		global $langs;

		// Check parameters
		if (empty($id) && empty($ref) && empty($track_id) && empty($email_msgid)) {
			$this->error = 'ErrorWrongParameters';
			dol_print_error(null, get_class($this)."::fetch ".$this->error);
			return -1;
		}

		$sql = "SELECT";
		$sql .= " t.rowid,";
		$sql .= " t.entity,";
		$sql .= " t.ref,";
		$sql .= " t.track_id,";
		$sql .= " t.fk_soc,";
		$sql .= " t.fk_project,";
		$sql .= " t.fk_contract,";
		$sql .= " t.origin_email,";
		$sql .= " t.origin_replyto,";
		$sql .= " t.origin_references,";
		$sql .= " t.fk_user_create,";
		$sql .= " t.fk_user_assign,";
		$sql .= " t.email_msgid,";
		$sql .= " t.email_date,";
		$sql .= " t.subject,";
		$sql .= " t.message,";
		$sql .= " t.note_private,";
		$sql .= " t.note_public,";
		$sql .= " t.fk_statut as status,";
		$sql .= " t.resolution,";
		$sql .= " t.progress,";
		$sql .= " t.timing,";
		$sql .= " t.type_code,";
		$sql .= " t.category_code,";
		$sql .= " t.severity_code,";
		$sql .= " t.datec,";
		$sql .= " t.date_read,";
		$sql .= " t.date_last_msg_sent,";
		$sql .= " t.date_close,";
		$sql .= " t.tms,";
		$sql .= " t.model_pdf,";
		$sql .= " t.extraparams,";
		$sql .= " t.ip,";
		$sql .= " type.label as type_label, category.label as category_label, severity.label as severity_label";
		$sql .= " FROM ".MAIN_DB_PREFIX."ticket as t";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_ticket_type as type ON type.code=t.type_code";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_ticket_category as category ON category.code=t.category_code";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_ticket_severity as severity ON severity.code=t.severity_code";

		if ($id) {
			$sql .= " WHERE t.rowid = ".((int) $id);
		} else {
			$sql .= " WHERE t.entity IN (".getEntity($this->element, 1).")";
			if (!empty($ref)) {
				$sql .= " AND t.ref = '".$this->db->escape($ref)."'";
			} elseif ($track_id) {
				$sql .= " AND t.track_id = '".$this->db->escape($track_id)."'";
			} else {
				$sql .= " AND t.email_msgid = '".$this->db->escape($email_msgid)."'";
			}
		}

		dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
		$resql = $this->db->query($sql);
		if ($resql) {
			if ($this->db->num_rows($resql)) {
				$obj = $this->db->fetch_object($resql);

				$this->id = $obj->rowid;
				$this->entity = $obj->entity;
				$this->ref = $obj->ref;
				$this->track_id = $obj->track_id;
				$this->fk_soc = $obj->fk_soc;
				$this->socid = $obj->fk_soc; // for fetch_thirdparty() method
				$this->fk_project = $obj->fk_project;
				$this->fk_contract = $obj->fk_contract;
				$this->origin_email = $obj->origin_email;
				$this->origin_replyto = $obj->origin_replyto;
				$this->origin_references = $obj->origin_references;
				$this->fk_user_create = $obj->fk_user_create;
				$this->fk_user_assign = $obj->fk_user_assign;
				$this->email_msgid = $obj->email_msgid;
				$this->email_date = $this->db->jdate($obj->email_date);
				$this->subject = $obj->subject;
				$this->message = $obj->message;
				$this->note_private = $obj->note_private;
				$this->note_public = $obj->note_public;
				$this->model_pdf = $obj->model_pdf;
				$this->extraparams = !empty($obj->extraparams) ? (array) json_decode($obj->extraparams, true) : array();
				$this->ip = $obj->ip;

				$this->status = $obj->status;
				$this->fk_statut = $this->status; // For backward compatibility

				$this->resolution = $obj->resolution;
				$this->progress = $obj->progress;
				$this->timing = $obj->timing;

				$this->type_code = $obj->type_code;
				$label_type = ($langs->trans("TicketTypeShort".$obj->type_code) != "TicketTypeShort".$obj->type_code ? $langs->trans("TicketTypeShort".$obj->type_code) : ($obj->type_label != '-' ? $obj->type_label : ''));
				$this->type_label = $label_type;

				$this->category_code = $obj->category_code;
				$label_category = ($langs->trans("TicketCategoryShort".$obj->category_code) != "TicketCategoryShort".$obj->category_code ? $langs->trans("TicketCategoryShort".$obj->category_code) : ($obj->category_label != '-' ? $obj->category_label : ''));
				$this->category_label = $label_category;

				$this->severity_code = $obj->severity_code;
				$label_severity = ($langs->trans("TicketSeverityShort".$obj->severity_code) != "TicketSeverityShort".$obj->severity_code ? $langs->trans("TicketSeverityShort".$obj->severity_code) : ($obj->severity_label != '-' ? $obj->severity_label : ''));
				$this->severity_label = $label_severity;

				$this->datec = $this->db->jdate($obj->datec);
				$this->date_creation = $this->db->jdate($obj->datec);
				$this->date_read = $this->db->jdate($obj->date_read);
				$this->date_validation = $this->db->jdate($obj->date_read);
				$this->date_last_msg_sent = $this->db->jdate($obj->date_last_msg_sent);
				$this->date_close = $this->db->jdate($obj->date_close);
				$this->tms = $this->db->jdate($obj->tms);
				$this->date_modification = $this->db->jdate($obj->tms);

				$this->fetch_optionals();

				$this->db->free($resql);
				return 1;
			} else {
				return 0;
			}
		} else {
			$this->error = "Error ".$this->db->lasterror();
			dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
			return -1;
		}
	}

	/**
	 * Load all objects in memory from database
	 *
	 * @param	User						$user		User for action
	 * @param	string						$sortorder	Sort order
	 * @param	string						$sortfield	Sort field
	 * @param	int							$limit		Limit
	 * @param	int							$offset		Offset page
	 * @param	int							$arch		Archive or not (not used)
	 * @param	string|array<string,int>	$filter		Filter for query
	 * @return	int 								Return integer <0 if KO, >0 if OK
	 */
	public function fetchAll($user, $sortorder = 'ASC', $sortfield = 't.datec', $limit = 0, $offset = 0, $arch = 0, $filter = '')
	{
		global $langs, $extrafields;

		// fetch optionals attributes and labels
		$extrafields->fetch_name_optionals_label($this->table_element);

		$sql = "SELECT";
		$sql .= " t.rowid,";
		$sql .= " t.ref,";
		$sql .= " t.track_id,";
		$sql .= " t.fk_soc,";
		$sql .= " t.fk_project,";
		$sql .= " t.fk_contract,";
		$sql .= " t.origin_email,";
		$sql .= " t.origin_replyto,";
		$sql .= " t.origin_references,";
		$sql .= " t.fk_user_create, uc.lastname as user_create_lastname, uc.firstname as user_create_firstname,";
		$sql .= " t.fk_user_assign, ua.lastname as user_assign_lastname, ua.firstname as user_assign_firstname,";
		$sql .= " t.subject,";
		$sql .= " t.message,";
		$sql .= " t.note_private,";
		$sql .= " t.note_public,";
		$sql .= " t.fk_statut as status,";
		$sql .= " t.resolution,";
		$sql .= " t.progress,";
		$sql .= " t.timing,";
		$sql .= " t.type_code,";
		$sql .= " t.category_code,";
		$sql .= " t.severity_code,";
		$sql .= " t.datec,";
		$sql .= " t.date_read,";
		$sql .= " t.date_last_msg_sent,";
		$sql .= " t.date_close,";
		$sql .= " t.tms,";
		$sql .= " type.label as type_label, category.label as category_label, severity.label as severity_label";
		// Add fields for extrafields
		if ($extrafields->attributes[$this->table_element]['count'] > 0) {
			foreach ($extrafields->attributes[$this->table_element]['label'] as $key => $val) {
				$sql .= ($extrafields->attributes[$this->table_element]['type'][$key] != 'separate' ? ",ef.".$key." as options_".$key : '');
			}
		}
		$sql .= " FROM ".MAIN_DB_PREFIX."ticket as t";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_ticket_type as type ON type.code = t.type_code";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_ticket_category as category ON category.code = t.category_code";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_ticket_severity as severity ON severity.code = t.severity_code";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = t.fk_soc";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as uc ON uc.rowid = t.fk_user_create";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as ua ON ua.rowid = t.fk_user_assign";
		if ($extrafields->attributes[$this->table_element]['count'] > 0) {
			if (is_array($extrafields->attributes[$this->table_element]['label']) && count($extrafields->attributes[$this->table_element]['label'])) {
				$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."ticket_extrafields as ef on (t.rowid = ef.fk_object)";
			}
		}
		$sql .= " WHERE t.entity IN (".getEntity('ticket').")";

		// Manage filter
		if (is_array($filter)) {
			foreach ($filter as $key => $value) {
				if (strpos($key, 'date')) { // To allow $filter['YEAR(s.dated)']=>$year
					$sql .= " AND ".$this->db->sanitize($key)." = '".$this->db->escape((string) $value)."'";
				} elseif (($key == 't.fk_user_assign') || ($key == 't.type_code') || ($key == 't.category_code') || ($key == 't.severity_code') || ($key == 't.fk_soc')) {
					$sql .= " AND ".$this->db->sanitize($key)." = '".$this->db->escape((string) $value)."'";
				} elseif ($key == 't.fk_statut') {
					if (is_array($value) && count($value) > 0) {
						$sql .= " AND ".$this->db->sanitize($key)." IN (".$this->db->sanitize(implode(',', $value)).")";
					} else {
						$sql .= " AND ".$this->db->sanitize($key).' = '.((int) $value);
					}
				} elseif ($key == 't.fk_contract') {
					$sql .= " AND ".$this->db->sanitize($key).' = '.((int) $value);
				} else {
					$sql .= " AND ".$this->db->sanitize($key)." LIKE '%".$this->db->escape($this->db->escapeforlike((string) $value))."%'";
				}
			}

			$filter = '';
		}

		// Manage filter
		$errormessage = '';
		$sql .= forgeSQLFromUniversalSearchCriteria($filter, $errormessage);
		if ($errormessage) {
			$this->errors[] = $errormessage;
			dol_syslog(__METHOD__.' '.implode(',', $this->errors), LOG_ERR);
			return -1;
		}

		// Case of external user
		$socid = $user->socid ?: 0;
		// If the internal user must only see his customers, force searching by him
		$search_sale = 0;
		if (empty($user->socid) && !$user->hasRight('societe', 'client', 'voir')) {
			$search_sale = $user->id;
		}
		// Search on sale representative
		if ($search_sale && $search_sale != '-1') {
			if ($search_sale == -2) {
				$sql .= " AND NOT EXISTS (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_soc = t.fk_soc)";
			} elseif ($search_sale > 0) {
				$sql .= " AND EXISTS (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_soc = t.fk_soc AND sc.fk_user = ".((int) $search_sale).")";
			}
		}
		// Search on socid
		if ($socid) {
			$sql .= " AND t.fk_soc = ".((int) $socid);
		}

		$sql .= $this->db->order($sortfield, $sortorder);
		if (!empty($limit)) {
			$sql .= $this->db->plimit($limit + 1, $offset);
		}

		dol_syslog(get_class($this)."::fetchAll", LOG_DEBUG);
		$resql = $this->db->query($sql);

		if ($resql) {
			$this->lines = array();

			$num = $this->db->num_rows($resql);
			$i = 0;

			if ($num) {
				while ($i < $num) {
					$obj = $this->db->fetch_object($resql);

					$line = new self($this->db);

					$line->id = $obj->rowid;
					//$line->rowid = $obj->rowid;
					$line->ref = $obj->ref;
					$line->track_id = $obj->track_id;
					$line->fk_soc = $obj->fk_soc;
					$line->fk_project = $obj->fk_project;
					$line->fk_contract = $obj->fk_contract;
					$line->origin_email = $obj->origin_email;
					$line->origin_replyto = $obj->origin_replyto;
					$line->origin_references = $obj->origin_references;

					$line->fk_user_create = $obj->fk_user_create;
					$line->fk_user_assign = $obj->fk_user_assign;

					$line->subject = $obj->subject;
					$line->message = $obj->message;
					$line->note_private = $obj->note_private;
					$line->note_public = $obj->note_public;
					$line->fk_statut = $obj->status;
					$line->status = $obj->status;
					$line->resolution = $obj->resolution;
					$line->progress = $obj->progress;
					$line->timing = $obj->timing;

					$label_type = ($langs->trans("TicketTypeShort".$obj->type_code) != "TicketTypeShort".$obj->type_code ? $langs->trans("TicketTypeShort".$obj->type_code) : ($obj->type_label != '-' ? $obj->type_label : ''));
					$line->type_label = $label_type;

					$this->category_code = $obj->category_code;
					$label_category = ($langs->trans("TicketCategoryShort".$obj->category_code) != "TicketCategoryShort".$obj->category_code ? $langs->trans("TicketCategoryShort".$obj->category_code) : ($obj->category_label != '-' ? $obj->category_label : ''));
					$line->category_label = $label_category;

					$this->severity_code = $obj->severity_code;
					$label_severity = ($langs->trans("TicketSeverityShort".$obj->severity_code) != "TicketSeverityShort".$obj->severity_code ? $langs->trans("TicketSeverityShort".$obj->severity_code) : ($obj->severity_label != '-' ? $obj->severity_label : ''));
					$line->severity_label = $label_severity;

					$line->datec = $this->db->jdate($obj->datec);
					$line->date_read = $this->db->jdate($obj->date_read);
					$line->date_last_msg_sent = $this->db->jdate($obj->date_last_msg_sent);
					$line->date_close = $this->db->jdate($obj->date_close);

					// Extra fields
					if ($extrafields->attributes[$this->table_element]['count'] > 0) {
						if (is_array($extrafields->attributes[$this->table_element]['label']) && count($extrafields->attributes[$this->table_element]['label'])) {
							foreach ($extrafields->attributes[$this->table_element]['label'] as $key => $val) {
								$tmpkey = 'options_'.$key;
								$line->{$tmpkey} = $obj->$tmpkey;
							}
						}
					}
					$this->lines[$i] = $line;
					$i++;
				}
			}
			$this->db->free($resql);
			return $num;
		} else {
			$this->error = "Error ".$this->db->lasterror();
			dol_syslog(get_class($this)."::fetchAll ".$this->error, LOG_ERR);
			return -1;
		}
	}

	/**
	 *  Update object into database
	 *
	 *  @param  User $user      User that modifies
	 *  @param  int  $notrigger 0=launch triggers after, 1=disable triggers
	 *  @return int                     Return integer <0 if KO, >0 if OK
	 */
	public function update($user, $notrigger = 0)
	{
		$error = 0;

		// $this->oldcopy should have been set by the caller of update
		//if (empty($this->oldcopy)) {
		//	dol_syslog("this->oldcopy should have been set by the caller of update (here properties were already modified)", LOG_WARNING);
		//	$this->oldcopy = dol_clone($this, 2);
		//}

		// Clean parameters
		if (isset($this->ref)) {
			$this->ref = trim($this->ref);
		}

		if (isset($this->track_id)) {
			$this->track_id = trim($this->track_id);
		}

		if (isset($this->fk_soc)) {
			$this->fk_soc = (int) $this->fk_soc;
		}

		if (isset($this->fk_project)) {
			$this->fk_project = (int) $this->fk_project;
		}

		if (isset($this->fk_contract)) {
			$this->fk_contract = (int) $this->fk_contract;
		}

		if (isset($this->origin_email)) {
			$this->origin_email = trim($this->origin_email);
		}

		if (isset($this->fk_user_create)) {
			$this->fk_user_create = (int) $this->fk_user_create;
		}

		if (isset($this->fk_user_assign)) {
			$this->fk_user_assign = (int) $this->fk_user_assign;
		}

		if (isset($this->subject)) {
			$this->subject = trim($this->subject);
		}

		if (isset($this->message)) {
			$this->message = trim($this->message);
			if (dol_strlen($this->message) > 65000) {
				global $langs;
				$langs->loadLangs(array('errors', 'ticket'));
				$this->errors[] = $langs->trans('ErrorFieldTooLong', $langs->transnoentitiesnoconv('InitialMessage'));
				dol_syslog(get_class($this).'::update error -1 message too long', LOG_ERR);
				return -1;
			}
		}

		if (isset($this->status)) {
			$this->status = (int) $this->status;
		}

		if (isset($this->resolution)) {
			$this->resolution = (int) $this->resolution;
		}

		if (isset($this->progress)) {
			$this->progress = (int) $this->progress;
		}

		if (isset($this->timing)) {
			$this->timing = trim($this->timing);
		}

		if (isset($this->type_code)) {
			$this->timing = trim($this->type_code);
		}

		if (isset($this->category_code)) {
			$this->timing = trim($this->category_code);
		}

		if (isset($this->severity_code)) {
			$this->timing = trim($this->severity_code);
		}
		if (isset($this->model_pdf)) {
			$this->model_pdf = trim($this->model_pdf);
		}
		// Check parameters
		// Put here code to add a control on parameters values
		// Update request
		$sql = "UPDATE ".MAIN_DB_PREFIX."ticket SET";
		$sql .= " ref=".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "").",";
		$sql .= " track_id=".(isset($this->track_id) ? "'".$this->db->escape($this->track_id)."'" : "null").",";
		$sql .= " fk_soc=".(isset($this->fk_soc) ? (int) $this->fk_soc : "null").",";
		$sql .= " fk_project=".(isset($this->fk_project) ? (int) $this->fk_project : "null").",";
		$sql .= " fk_contract=".(isset($this->fk_contract) ? (int) $this->fk_contract : "null").",";
		$sql .= " origin_email=".(isset($this->origin_email) ? "'".$this->db->escape($this->origin_email)."'" : "null").",";
		$sql .= " origin_replyto=".(isset($this->origin_replyto) ? "'".$this->db->escape($this->origin_replyto)."'" : "null").",";
		$sql .= " origin_references=".(isset($this->origin_references) ? "'".$this->db->escape($this->origin_references)."'" : "null").",";
		$sql .= " fk_user_create=".(isset($this->fk_user_create) ? (int) $this->fk_user_create : "null").",";
		$sql .= " fk_user_assign=".(isset($this->fk_user_assign) ? (int) $this->fk_user_assign : "null").",";
		$sql .= " subject=".(isset($this->subject) ? "'".$this->db->escape($this->subject)."'" : "null").",";
		$sql .= " message=".(isset($this->message) ? "'".$this->db->escape($this->message)."'" : "null").",";
		$sql .= " note_private=".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").",";
		$sql .= " note_public=".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").",";
		$sql .= " fk_statut=".(isset($this->status) ? (int) $this->status : "0").",";
		$sql .= " resolution=".(isset($this->resolution) ? (int) $this->resolution : "null").",";
		$sql .= " progress=".(isset($this->progress) ? "'".$this->db->escape((string) $this->progress)."'" : "null").",";
		$sql .= " timing=".(isset($this->timing) ? "'".$this->db->escape($this->timing)."'" : "null").",";
		$sql .= " type_code=".(isset($this->type_code) ? "'".$this->db->escape($this->type_code)."'" : "null").",";
		$sql .= " category_code=".(isset($this->category_code) ? "'".$this->db->escape($this->category_code)."'" : "null").",";
		$sql .= " severity_code=".(isset($this->severity_code) ? "'".$this->db->escape($this->severity_code)."'" : "null").",";
		$sql .= " datec=".(isDolTms($this->datec) ? "'".$this->db->idate($this->datec)."'" : 'null').",";
		$sql .= " date_read=".(dol_strlen((string) $this->date_read) != 0 ? "'".$this->db->idate($this->date_read)."'" : 'null').",";
		$sql .= " date_last_msg_sent=".(dol_strlen((string) $this->date_last_msg_sent) != 0 ? "'".$this->db->idate($this->date_last_msg_sent)."'" : 'null').",";
		$sql .= " model_pdf=".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").",";
		$sql .= " date_close=".(dol_strlen((string) $this->date_close) != 0 ? "'".$this->db->idate($this->date_close)."'" : 'null');
		$sql .= " WHERE rowid=".((int) $this->id);

		$this->db->begin();

		$resql = $this->db->query($sql);
		if (!$resql) {
			$error++;
			$this->errors[] = "Error ".$this->db->lasterror();
		}

		if (!$error) {
			// Update extrafields
			$result = $this->insertExtraFields();
			if ($result < 0) {
				$error++;
			}
		}

		if (!$error && !$notrigger) {
			// Call trigger
			$result = $this->call_trigger('TICKET_MODIFY', $user);
			if ($result < 0) {
				$error++;
			}
			// End call triggers
		}

		// Commit or rollback
		if ($error) {
			foreach ($this->errors as $errmsg) {
				dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
				$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
			}
			$this->db->rollback();
			return -1 * $error;
		} else {
			$this->db->commit();
			return 1;
		}
	}

	/**
	 *  Delete object in database
	 *
	 *     @param  User $user      User that deletes
	 *  @param  int  $notrigger 0=launch triggers after, 1=disable triggers
	 *  @return int                     Return integer <0 if KO, >0 if OK
	 */
	public function delete($user, $notrigger = 0)
	{
		$error = 0;

		$this->db->begin();

		if (!$notrigger) {
			// Call trigger
			$result = $this->call_trigger('TICKET_DELETE', $user);
			if ($result < 0) {
				$error++;
			}
			// End call triggers
		}

		if (!$error) {
			// Delete linked contacts
			$res = $this->delete_linked_contact();
			if ($res < 0) {
				dol_syslog(get_class($this)."::delete error", LOG_ERR);
				$error++;
			}
		}

		if (!$error) {
			// Delete linked object
			$res = $this->deleteObjectLinked();
			if ($res < 0) {
				$error++;
			}
		}

		// Removed extrafields
		if (!$error) {
			$result = $this->deleteExtraFields();
			if ($result < 0) {
				$error++;
				dol_syslog(get_class($this)."::delete error -3 ".$this->error, LOG_ERR);
			}
		}

		// Delete all child tables

		if (!$error) {
			$sql = "DELETE FROM ".MAIN_DB_PREFIX."categorie_ticket";
			$sql .= " WHERE fk_ticket = ".(int) $this->id;

			$result = $this->db->query($sql);
			if (!$result) {
				$error++;
				$this->errors[] = $this->db->lasterror();
			}
		}

		if (!$error) {
			$sql = "DELETE FROM ".MAIN_DB_PREFIX."ticket";
			$sql .= " WHERE rowid=".((int) $this->id);

			dol_syslog(get_class($this)."::delete sql=".$sql);
			$resql = $this->db->query($sql);
			if (!$resql) {
				$error++;
				$this->errors[] = "Error ".$this->db->lasterror();
			} else {
				// we delete file with dol_delete_dir_recursive
				$this->deleteEcmFiles(1);

				$dir = DOL_DATA_ROOT.'/'.$this->element.'/'.$this->ref;
				// For remove dir
				if (dol_is_dir($dir)) {
					if (!dol_delete_dir_recursive($dir)) {
						$this->errors[] = $this->error;
					}
				}
			}
		}

		// Commit or rollback
		if ($error) {
			foreach ($this->errors as $errmsg) {
				dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
				$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
			}
			$this->db->rollback();
			return -1 * $error;
		} else {
			$this->db->commit();
			return 1;
		}
	}

	/**
	 *     Load an object from its id and create a new one in database
	 *
	 *     @param   User    $user       User that clone
	 *     @param   int     $fromid     Id of object to clone
	 *     @return  int                 New id of clone
	 */
	public function createFromClone(User $user, $fromid)
	{
		$error = 0;

		$object = new Ticket($this->db);

		$this->db->begin();

		// Load source object
		$object->fetch($fromid);

		// Clear fields
		$object->id = 0;
		$object->statut = 0;
		$object->status = 0;
		$object->ref = $object->getDefaultRef();
		$object->track_id = generate_random_id(16);
		$object->progress = 0;
		// Reset lifecycle timestamps so the clone starts fresh: datec is filled by
		// create() with dol_now() when empty, date_read and date_close stay null
		// because the new ticket has not been read or closed yet (see issue #38559).
		// Reset lifecycle timestamps and resolution so the clone starts fresh.
		// datec stays at 0 so Ticket::create()'s `if (empty($this->datec))` guard
		// fills it with dol_now(). date_read / date_close / resolution are unset
		// so the SQL writers' `!isset` checks emit NULL. We use these forms
		// rather than `= null` so phpstan stays happy with the int-typed
		// property declarations.
		$object->datec = 0;
		unset($object->date_read);
		unset($object->date_close);
		unset($object->resolution);

		// Create clone
		$object->context['createfromclone'] = 'createfromclone';
		$result = $object->create($user);

		// Other options
		if ($result < 0) {
			$this->error = $object->error;
			$error++;
		}

		unset($object->context['createfromclone']);

		// End
		if (!$error) {
			$this->db->commit();
			return $object->id;
		} else {
			$this->db->rollback();
			return -1;
		}
	}

	/**
	 *     Initialise object with example values
	 *     Id must be 0 if object instance is a specimen
	 *
	 *     @return int
	 */
	public function initAsSpecimen()
	{
		$this->id = 0;
		$this->entity = 1;
		$this->ref = 'TI0501-001';
		$this->track_id = 'XXXXaaaa';
		$this->origin_email = 'email@email.com';
		$this->fk_project = 1;
		$this->fk_user_create = 1;
		$this->fk_user_assign = 1;
		$this->subject = 'Subject of ticket';
		$this->message = 'Message of ticket';
		$this->status = 0;
		$this->resolution = 1;
		$this->progress = 10;
		// $this->timing = '30';
		$this->type_code = 'TYPECODE';
		$this->category_code = 'CATEGORYCODE';
		$this->severity_code = 'SEVERITYCODE';
		$this->datec = dol_now();
		$this->date_read = dol_now();
		$this->date_last_msg_sent = dol_now();
		$this->date_close = dol_now();
		$this->tms = dol_now();

		return 1;
	}

	/**
	 * Print selected status
	 *
	 * @param 	string    $selected   	Selected status
	 * @return 	void
	 */
	public function printSelectStatus($selected = "")
	{
		print Form::selectarray('search_fk_statut', $this->labelStatusShort, $selected, $show_empty = 1, $key_in_label = 0, $value_as_key = 0, $option = '', $translate = 1, $maxlen = 0, $disabled = 0, $sort = '', $morecss = '');
	}


	/**
	 * Load into a cache the types of tickets (setup done into dictionaries)
	 *
	 * @return 	int       Number of lines loaded, 0 if already loaded, <0 if KO
	 */
	public function loadCacheTypesTickets()
	{
		global $langs;

		if (!empty($this->cache_types_tickets) && count($this->cache_types_tickets)) {
			return 0;
		}
		// Cache deja charge

		$sql = "SELECT rowid, code, label, use_default, pos, description";
		$sql .= " FROM ".MAIN_DB_PREFIX."c_ticket_type";
		$sql .= " WHERE entity IN (".getEntity('c_ticket_type').")";
		$sql .= " AND active > 0";
		$sql .= " ORDER BY pos";
		dol_syslog(get_class($this)."::load_cache_type_tickets", LOG_DEBUG);
		$resql = $this->db->query($sql);
		if ($resql) {
			$num = $this->db->num_rows($resql);
			$i = 0;
			while ($i < $num) {
				$obj = $this->db->fetch_object($resql);
				$label = ($langs->trans("TicketTypeShort".$obj->code) != "TicketTypeShort".$obj->code ? $langs->trans("TicketTypeShort".$obj->code) : ($obj->label != '-' ? $obj->label : ''));
				$this->cache_types_tickets[$obj->rowid]['code'] = $obj->code;
				$this->cache_types_tickets[$obj->rowid]['label'] = $label;
				$this->cache_types_tickets[$obj->rowid]['use_default'] = $obj->use_default;
				$this->cache_types_tickets[$obj->rowid]['pos'] = $obj->pos;
				$i++;
			}
			return $num;
		} else {
			dol_print_error($this->db);
			return -1;
		}
	}

	/**
	 *      Load into a cache array, the list of ticket categories (setup done into dictionary)
	 *
	 *      @param	int<-1,1>	$publicgroup	0=No public group, 1=Public group only, -1=All
	 *      @return int        		     		Number of lines loaded, 0 if already loaded, <0 if KO
	 */
	public function loadCacheCategoriesTickets($publicgroup = -1)
	{
		global $conf, $langs;

		if ($publicgroup == -1 && !empty($conf->cache['category_tickets']) && count($conf->cache['category_tickets'])) {
			// Cache already loaded
			return 0;
		}

		$sql = "SELECT rowid, code, label, use_default, pos, description, public, active, force_severity, fk_parent";
		$sql .= " FROM ".MAIN_DB_PREFIX."c_ticket_category";
		$sql .= " WHERE entity IN (".getEntity('c_ticket_category').")";
		$sql .= " AND active > 0";
		if ($publicgroup > -1) {
			$sql .= " AND public = ".((int) $publicgroup);
		}
		$sql .= " ORDER BY pos";

		dol_syslog(get_class($this)."::load_cache_categories_tickets", LOG_DEBUG);

		$resql = $this->db->query($sql);
		if ($resql) {
			$num = $this->db->num_rows($resql);
			$i = 0;
			while ($i < $num) {
				$obj = $this->db->fetch_object($resql);
				$conf->cache['category_tickets'][$obj->rowid]['code'] = $obj->code;
				$conf->cache['category_tickets'][$obj->rowid]['use_default'] = $obj->use_default;
				$conf->cache['category_tickets'][$obj->rowid]['pos'] = $obj->pos;
				$conf->cache['category_tickets'][$obj->rowid]['public'] = $obj->public;
				$conf->cache['category_tickets'][$obj->rowid]['active'] = $obj->active;
				$conf->cache['category_tickets'][$obj->rowid]['force_severity'] = $obj->force_severity;
				$conf->cache['category_tickets'][$obj->rowid]['fk_parent'] = $obj->fk_parent;

				// If  translation exists, we use it to store already translated string.
				// Warning: You should not use this and recompute the translated string into caller code to get the value into expected language
				$label = ($langs->trans("TicketCategoryShort".$obj->code) != "TicketCategoryShort".$obj->code ? $langs->trans("TicketCategoryShort".$obj->code) : ($obj->label != '-' ? $obj->label : ''));
				$conf->cache['category_tickets'][$obj->rowid]['label'] = $label;

				$i++;
			}
			return $num;
		} else {
			dol_print_error($this->db);
			return -1;
		}
	}

	/**
	 *      Charge dans cache la liste des sévérité de tickets (paramétrable dans dictionnaire)
	 *
	 *      @return int             Number of lines loaded, 0 if already loaded, <0 if KO
	 */
	public function loadCacheSeveritiesTickets()
	{
		global $conf, $langs;

		if (!empty($conf->cache['severity_tickets']) && count($conf->cache['severity_tickets'])) {
			// Cache already loaded
			return 0;
		}

		$sql = "SELECT rowid, code, label, use_default, pos, description";
		$sql .= " FROM ".MAIN_DB_PREFIX."c_ticket_severity";
		$sql .= " WHERE entity IN (".getEntity('c_ticket_severity').")";
		$sql .= " AND active > 0";
		$sql .= " ORDER BY pos";
		dol_syslog(get_class($this)."::loadCacheSeveritiesTickets", LOG_DEBUG);
		$resql = $this->db->query($sql);
		if ($resql) {
			$num = $this->db->num_rows($resql);
			$i = 0;
			while ($i < $num) {
				$obj = $this->db->fetch_object($resql);

				$conf->cache['severity_tickets'][$obj->rowid]['code'] = $obj->code;
				$label = ($langs->trans("TicketSeverityShort".$obj->code) != "TicketSeverityShort".$obj->code ? $langs->trans("TicketSeverityShort".$obj->code) : ($obj->label != '-' ? $obj->label : ''));
				$conf->cache['severity_tickets'][$obj->rowid]['label'] = $label;
				$conf->cache['severity_tickets'][$obj->rowid]['use_default'] = $obj->use_default;
				$conf->cache['severity_tickets'][$obj->rowid]['pos'] = $obj->pos;
				$i++;
			}
			return $num;
		} else {
			dol_print_error($this->db);
			return -1;
		}
	}


	/**
	 * Return status label of object
	 *
	 * @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
	 * @return     	string    			Label
	 */
	public function getLibStatut($mode = 0)
	{
		return $this->LibStatut($this->status, $mode, 0, $this->progress);
	}


	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
	/**
	 * Return status label of object
	 *
	 * @param	int			$status			Id status
	 * @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
	 * @param	int			$notooltip		1=No tooltip
	 * @param	int			$progress		Progression (0 to 100)
	 * @return	string						Label
	 */
	public function LibStatut($status, $mode = 0, $notooltip = 0, $progress = 0)
	{
		// phpcs:enable
		global $langs, $hookmanager;

		$labelStatus = (isset($status) && !empty($this->labelStatus[$status])) ? $this->labelStatus[$status] : '';
		$labelStatusShort = (isset($status) && !empty($this->labelStatusShort[$status])) ? $this->labelStatusShort[$status] : '';
		$statusType = '';

		$isUnknown = false;
		switch ($status) {
			case self::STATUS_NOT_READ:						// Not read
				$statusType = 'status0';
				break;
			case self::STATUS_READ:							// Read
				$statusType = 'status1';
				break;
			case self::STATUS_ASSIGNED:						// Assigned
				$statusType = 'status2';
				break;
			case self::STATUS_IN_PROGRESS:					// In progress
				$statusType = 'status4';
				break;
			case self::STATUS_WAITING:						// Waiting/pending/suspended
				$statusType = 'status7';
				break;
			case self::STATUS_NEED_MORE_INFO:				// Waiting more information from the requester
				$statusType = 'status3';
				break;
			case self::STATUS_CANCELED:						// Canceled
				$statusType = 'status9';
				break;
			case self::STATUS_CLOSED:						// Closed
				$statusType = 'status6';
				break;
			default:
				$isUnknown = true;
				break;
		}

		$parameters = array(
			'status'          => $status,
			'mode'            => $mode,
		);

		// Note that $action and $object may have been modified by hook
		$reshook = $hookmanager->executeHooks('LibStatut', $parameters, $this);

		if ($reshook > 0) {
			return $hookmanager->resPrint;
		}

		if ($isUnknown) {
			$labelStatus = 'Unknown';
			$labelStatusShort = 'Unknown';
			$statusType = 'status0';
			$mode = 0;
		}

		$params = array();
		if ($notooltip) {
			$params = array('tooltip' => 'no');
		}

		$labelStatus = $langs->transnoentitiesnoconv($labelStatus);
		$labelStatusShort = $langs->transnoentitiesnoconv($labelStatusShort);

		if ($status == self::STATUS_IN_PROGRESS && $progress > 0) {
			$labelStatus .= ' ('.round($progress).'%)';
			$labelStatusShort .= ' ('.round($progress).'%)';
		}

		return dolGetStatus($labelStatus, $labelStatusShort, '', $statusType, $mode, '', $params);
	}

	/**
	 * getTooltipContentArray
	 * @param array<string,mixed> $params params to construct tooltip data
	 * @since v18
	 * @return array{picto?:string,ref?:string,refsupplier?:string,label?:string,date?:string,date_echeance?:string,amountht?:string,total_ht?:string,totaltva?:string,amountlt1?:string,amountlt2?:string,amountrevenustamp?:string,totalttc?:string}|array{optimize:string}
	 */
	public function getTooltipContentArray($params)
	{
		global $langs;

		$langs->load('ticket');
		$nofetch = !empty($params['nofetch']);

		$datas = array();
		$datas['picto'] = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("Ticket").'</u>';
		$datas['picto'] .= ' '.$this->getLibStatut(4);
		$datas['ref'] = '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
		$datas['track_id'] = '<br><b>'.$langs->trans('TicketTrackId').':</b> '.$this->track_id;
		$datas['subject'] = '<br><b>'.$langs->trans('Subject').':</b> '.$this->subject;
		if ($this->date_creation) {
			$datas['date_creation'] = '<br><b>'.$langs->trans('DateCreation').':</b> '.dol_print_date($this->date_creation, 'dayhour');
		}
		if ($this->date_modification) {
			$datas['date_modification'] = '<br><b>'.$langs->trans('DateModification').':</b> '.dol_print_date($this->date_modification, 'dayhour');
		}
		// show categories for this record only in ajax to not overload lists
		if (isModEnabled('category') && !$nofetch) {
			require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
			$form = new Form($this->db);
			$datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_TICKET, 1);
		}

		return $datas;
	}

	/**
	 *  Return a link to the object card (with optionally the picto)
	 *
	 *	@param	int		$withpicto					Include picto in link (0=No picto, 1=Include picto into link, 2=Only picto)
	 *	@param	string	$option						On what the link point to ('nolink', ...)
	 *  @param	int  	$notooltip					1=Disable tooltip
	 *  @param  string  $morecss            		Add more css on link
	 *  @param  int     $save_lastsearch_value    	-1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
	 *	@return	string								String with URL
	 */
	public function getNomUrl($withpicto = 0, $option = '', $notooltip = 0, $morecss = '', $save_lastsearch_value = -1)
	{
		global $action, $conf, $hookmanager, $langs;

		if (!empty($conf->dol_no_mouse_hover)) {
			$notooltip = 1; // Force disable tooltips
		}

		$result = '';

		$params = [
			'id' => $this->id,
			'objecttype' => $this->element,
			'option' => $option,
			'nofetch' => 1,
		];
		$classfortooltip = 'classfortooltip';
		$dataparams = '';
		if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
			$classfortooltip = 'classforajaxtooltip';
			$dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
			$label = '';
		} else {
			$label = implode($this->getTooltipContentArray($params));
		}

		$url = DOL_URL_ROOT.'/ticket/card.php?id='.$this->id;

		if ($option != 'nolink') {
			// Add param to save lastsearch_values or not
			$add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
			if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
				$add_save_lastsearch_values = 1;
			}
			if ($add_save_lastsearch_values) {
				$url .= '&save_lastsearch_values=1';
			}
		}

		$linkclose = '';
		if (empty($notooltip)) {
			if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
				$label = $langs->trans("ShowTicket");
				$linkclose .= ' alt="'.dolPrintHTMLForAttribute($label).'"';
			}
			$linkclose .= ($label ? ' title="'.dolPrintHTMLForAttribute($label).'"' : ' title="tocomplete"');
			$linkclose .= $dataparams.' class="'.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
		} else {
			$linkclose = ($morecss ? ' class="'.$morecss.'"' : '');
		}

		$linkstart = '<a href="'.$url.'"';
		$linkstart .= $linkclose.'>';
		$linkend = '</a>';

		$result .= $linkstart;
		if ($withpicto) {
			$result .= img_object(($notooltip ? '' : $label), ($this->picto ?: 'generic'), (($withpicto != 2) ? 'class="paddingright"' : ''), 0, 0, $notooltip ? 0 : 1);
		}
		if ($withpicto != 2) {
			$result .= $this->ref;
		}
		$result .= $linkend;
		//if ($withpicto != 2) $result.=(($addlabel && $this->label) ? $sep . dol_trunc($this->label, ($addlabel > 1 ? $addlabel : 0)) : '');

		$hookmanager->initHooks(array('ticketdao'));
		$parameters = array('id' => $this->id, 'getnomurl' => &$result);
		$reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
		if ($reshook > 0) {
			$result = $hookmanager->resPrint;
		} else {
			$result .= $hookmanager->resPrint;
		}

		return $result;
	}


	/**
	 *    Mark a message as read
	 *
	 *    @param    User		$user			Object user
	 *    @param	int			$notrigger		No trigger
	 *    @return   int							Return integer <0 if KO, 0=nothing done, >0 if OK
	 */
	public function markAsRead($user, $notrigger = 0)
	{
		global $langs;

		$error = 0;

		if ($this->status != self::STATUS_CANCELED) { // no closed
			$this->oldcopy = dol_clone($this, 2);

			$this->db->begin();

			$this->status = Ticket::STATUS_READ;

			$sql = "UPDATE ".MAIN_DB_PREFIX."ticket";
			$sql .= " SET fk_statut = ".((int) $this->status) .", date_read = '".$this->db->idate(dol_now())."'";
			$sql .= " WHERE rowid = ".((int) $this->id);

			dol_syslog(get_class($this)."::markAsRead");
			$resql = $this->db->query($sql);
			if ($resql) {
				$this->context['actionmsg'] = $langs->trans('TicketLogMesgReadBy', $this->ref, $user->getFullName($langs));
				$this->context['actionmsg2'] = $langs->trans('TicketLogMesgReadBy', $this->ref, $user->getFullName($langs));

				if (!$notrigger) {
					// Call trigger
					$result = $this->call_trigger('TICKET_MODIFY', $user);
					if ($result < 0) {
						$error++;
					}
					// End call triggers
				}

				if (!$error) {
					$this->db->commit();
					return 1;
				} else {
					$this->status = $this->oldcopy->status;

					$this->db->rollback();

					$this->error = implode(',', $this->errors);

					dol_syslog(get_class($this)."::markAsRead ".$this->error, LOG_ERR);
					return -1;
				}
			} else {
				$this->status = $this->oldcopy->status;

				$this->db->rollback();
				$this->error = $this->db->lasterror();
				dol_syslog(get_class($this)."::markAsRead ".$this->error, LOG_ERR);
				return -1;
			}
		}

		return 0;
	}

	/**
	 *   Set read date if not already set.
	 *
	 * @param   User    $user       User performing the action
	 * @param   int     $notrigger  Disable triggers
	 * @return  int                 Return -1 if error, 0 if nothing done, 1 if success
	 */
	public function setReadDate($user, $notrigger = 0)
	{
		global $langs;

		if (!empty($this->date_read)) {
			return 0;
		}

		$this->oldcopy = dol_clone($this, 2);

		$this->db->begin();

		$sql = "UPDATE ".MAIN_DB_PREFIX."ticket";
		$sql .= " SET date_read = '".$this->db->idate(dol_now())."'";
		$sql .= ", fk_user_modif = ".((int) $user->id);
		$sql .= " WHERE rowid = ".((int) $this->id);

		dol_syslog(get_class($this)."::setReadDate");

		$resql = $this->db->query($sql);

		if (!$resql) {
			$this->db->rollback();
			$this->date_read = $this->oldcopy->date_read;
			$this->error = $this->db->lasterror();

			dol_syslog(get_class($this)."::setReadDate ".$this->error, LOG_ERR);
			return -1;
		}

		if (!$notrigger) {
			$result = $this->call_trigger('TICKET_MODIFY', $user);
			if ($result < 0) {
				$this->db->rollback();
				$this->date_read = $this->oldcopy->date_read;
				$this->error = implode(',', $this->errors);

				dol_syslog(get_class($this)."::setReadDate ".$this->error, LOG_ERR);
				return -1;
			}
		}

		$this->db->commit();

		$this->date_read = dol_now();
		$this->fk_user_modif = $user->id;
		$this->user_modification_id = $user->id;

		return 1;
	}

	/**
	 *    Set an assigned user to a ticket.
	 *
	 *    @param    User		$user				Object user
	 *    @param    int 		$id_assign_user		ID of user assigned
	 *    @param    int<0,1>	$notrigger        	Disable trigger
	 *    @return   int								Return integer <0 if KO, 0=Nothing done, >0 if OK
	 */
	public function assignUser($user, $id_assign_user, $notrigger = 0)
	{
		$error = 0;

		$this->oldcopy = dol_clone($this, 2);

		$this->db->begin();

		$sql = "UPDATE ".MAIN_DB_PREFIX."ticket";
		if ($id_assign_user > 0) {
			$newstatus = Ticket::STATUS_ASSIGNED;
			if (getDolGlobalString('TICKET_AUTO_READ_WHEN_ASSIGN')) {
				$newstatus = Ticket::STATUS_READ;
			}
			$sql .= " SET fk_user_assign=".((int) $id_assign_user).", fk_statut = ".$newstatus;
		} else {
			$sql .= " SET fk_user_assign=null, fk_statut = ".Ticket::STATUS_READ;
		}
		$sql .= " WHERE rowid = ".((int) $this->id);

		dol_syslog(get_class($this)."::assignUser sql=".$sql);
		$resql = $this->db->query($sql);
		if ($resql) {
			$this->fk_user_assign = $id_assign_user; // May be used by trigger

			if (!$notrigger) {
				// Call trigger
				$result = $this->call_trigger('TICKET_ASSIGNED', $user);
				if ($result < 0) {
					$error++;
				}
				// End call triggers
			}

			if (!$error) {
				$this->db->commit();
				return 1;
			} else {
				$this->db->rollback();
				$this->error = implode(',', $this->errors);
				dol_syslog(get_class($this)."::assignUser ".$this->error, LOG_ERR);
				return -1;
			}
		} else {
			$this->db->rollback();
			$this->error = $this->db->lasterror();
			dol_syslog(get_class($this)."::assignUser ".$this->error, LOG_ERR);
			return -1;
		}
	}

	/**
	 * Add message into database
	 *
	 * @param	User		$user				User that creates
	 * @param	int<0,1>	$notrigger			0=launch triggers after, 1=disable triggers
	 * @param	string[]	$filename_list		List of files to attach (full path of filename on file system)
	 * @param	string[]	$mimetype_list		List of MIME type of attached files
	 * @param	string[]	$mimefilename_list	List of attached file name in message
	 * @param	bool		$send_email			Whether the message is sent by email
	 * @param	int<0,1>	$public_area		0=Default, 1 if we are creating the message from a public area (so we can search contact from email to add it as contact of ticket if TICKET_ASSIGN_CONTACT_TO_MESSAGE is set)
	 * @param	string		$summary			''=Default means using subject, else insert the summary as label
	 * @param 	array<int,array{id:int,mandatory:int<0,1>,answer_status:int,transparency:int<0,1>}|int>		$external_contacts	List of external contacts associated with the ticket
	 * @return	int								Return integer <0 if KO, ID of actioncomm create if OK
	 */
	public function createTicketMessage($user, $notrigger = 0, $filename_list = array(), $mimetype_list = array(), $mimefilename_list = array(), $send_email = false, $public_area = 0, $summary = '', $external_contacts = [])
	{
		global $conf, $langs, $hookmanager;
		$error = 0;

		$now = dol_now();

		// Clean parameters
		if (isset($this->track_id)) {
			$this->track_id = trim($this->track_id);
		}

		if (isset($this->message)) {
			$this->message = trim($this->message);
		}

		$this->db->begin();

		// Insert entry into agenda with code 'TICKET_MSG'
		include_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php';
		$actioncomm = new ActionComm($this->db);
		$actioncomm->type_code = 'AC_OTH_AUTO';	// This is not an entry that must appears into manual calendar but only into CRM calendar
		$actioncomm->code = 'TICKET_MSG';
		if ($this->private) {
			$actioncomm->code = 'TICKET_MSG_PRIVATE';
		}
		if ($send_email) {
			$actioncomm->code .= '_SENTBYMAIL';
		}
		if ((empty($user->id) || $user->id == 0) && isset($_SESSION['email_customer'])) {
			$actioncomm->email_from = $_SESSION['email_customer'];
		}
		$actioncomm->socid = $this->socid;
		$actioncomm->label = $this->subject;
		if ($summary) {
			$actioncomm->label = preg_replace('/(\[[^\]]*\]).*$/', '\1', $actioncomm->label);
			if ($actioncomm->label) {
				$actioncomm->label .= ' ';
			}
			$actioncomm->label .= (string) $summary;
		}

		$actioncomm->note_private = $this->message;
		$actioncomm->userassigned = array($user->id => array('id' => $user->id,'transparency' => 0));

		$parameters = array();
		$reshook = $hookmanager->executeHooks('createTicketMessageExternalContacts', $parameters, $actioncomm);
		if ($reshook < 0) {
			$actioncomm->socpeopleassigned = $external_contacts;
		} elseif ($reshook == 0) {
			$actioncomm->socpeopleassigned += $external_contacts;
		}

		$actioncomm->userownerid = $user->id;
		$actioncomm->datep = $now;
		$actioncomm->percentage = -1; // percentage is not relevant for punctual events
		$actioncomm->elementtype = 'ticket';
		$actioncomm->fk_element = $this->id;
		$actioncomm->elementid = $this->id;
		$actioncomm->elementtype = 'ticket';
		$actioncomm->fk_project = $this->fk_project;

		// Add first contact id found in database from submitter email entered into public interface
		// Feature disabled by default: This has a security trouble. The public interface is a no login interface, so being able to show the contact info from an
		// email decided by the submiter allows anybody to get information on any contact (customer or supplier) in Dolibarr database.
		// He can even check if contact exists by trying any email if this feature is enabled.
		if ($public_area && !empty($this->origin_email) && getDolGlobalString('TICKET_ASSIGN_CONTACT_TO_MESSAGE')) {
			$contacts = $this->searchContactByEmail($this->origin_email);
			if (!empty($contacts)) {
				// Ensure that contact is active and select first active contact
				foreach ($contacts as $contact) {
					if ((int) $contact->status == 1) {
						$actioncomm->contact_id = $contact->id;
						break;
					}
				}
			}
		}

		$attachedfiles = array();
		$attachedfiles['paths'] = $filename_list;
		$attachedfiles['names'] = $mimefilename_list;
		$attachedfiles['mimes'] = $mimetype_list;
		if (is_array($attachedfiles) && count($attachedfiles) > 0) {
			$actioncomm->attachedfiles = $attachedfiles;
		}

		//if (!empty($mimefilename_list) && is_array($mimefilename_list)) {
		//	$actioncomm->note_private = dol_concatdesc($actioncomm->note_private, "\n".$langs->transnoentities("AttachedFiles").': '.implode(';', $mimefilename_list));
		//}
		$actionid = $actioncomm->create($user);
		if ($actionid <= 0) {
			$error++;
			$this->error = $actioncomm->error;
			$this->errors = $actioncomm->errors;
		}

		if ($actionid > 0) {
			if (is_array($attachedfiles) && array_key_exists('paths', $attachedfiles) && count($attachedfiles['paths']) > 0) {
				// If there is some files, we must now link them to the event, so we can show them per event.
				foreach ($attachedfiles['paths'] as $key => $filespath) {
					// Disabled the move into another directory, Files for a ticket should be stored into ticket directory. It generates too much troubles.
					$destdir = $conf->ticket->dir_output.'/'.$this->ref;
					//$destfile = $destdir.'/'.$attachedfiles['names'][$key];
					//if (dol_mkdir($destdir) >= 0) {
					//require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
					//dol_move($filespath, $destfile);	// Disabled, a file for a ticket should be stored into ticket directory. It generates big trouble.
					if (in_array($actioncomm->code, array('TICKET_MSG', 'TICKET_MSG_SENTBYMAIL', 'TICKET_MSG_PRIVATE'))) {
						$ecmfile = new EcmFiles($this->db);
						$destdir = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $destdir);
						$destdir = preg_replace('/[\\/]$/', '', $destdir);
						$destdir = preg_replace('/^[\\/]/', '', $destdir);

						$result = $ecmfile->fetch(0, '', $destdir.'/'.$attachedfiles['names'][$key]);

						// We also store the ID of event.
						$ecmfile->agenda_id = $actionid;

						if ($result > 0) {
							$result = $ecmfile->update($user);
							if ($result < 0) {
								setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
							}
						}
					}
					//}
				}
			}
		}

		// Commit or rollback
		if ($error) {
			$this->db->rollback();
			return -1 * $error;
		} else {
			$this->db->commit();
			return $actionid;
		}
	}

	/**
	 *      Load the list of event on ticket into ->cache_msgs_ticket
	 *
	 *      @return int             Number of lines loaded, 0 if already loaded, <0 if KO
	 */
	public function loadCacheMsgsTicket()
	{
		if (!empty($this->cache_msgs_ticket) && is_array($this->cache_msgs_ticket) && count($this->cache_msgs_ticket)) {
			return 0;
		}

		// Cache already loaded

		$sql = "SELECT id as rowid, fk_user_author, fk_user_action, email_from, datec, datep, label, note as message, code";
		$sql .= " FROM ".MAIN_DB_PREFIX."actioncomm";
		$sql .= " WHERE fk_element = ".(int) $this->id;
		$sql .= " AND elementtype = 'ticket'";
		$sql .= " ORDER BY datep DESC";

		dol_syslog(get_class($this)."::load_cache_actions_ticket", LOG_DEBUG);
		$resql = $this->db->query($sql);
		if ($resql) {
			$num = $this->db->num_rows($resql);
			$i = 0;
			while ($i < $num) {
				$obj = $this->db->fetch_object($resql);
				$this->cache_msgs_ticket[$i]['id'] = $obj->rowid;
				$this->cache_msgs_ticket[$i]['fk_user_author'] = $obj->fk_user_author;
				$this->cache_msgs_ticket[$i]['fk_user_action'] = $obj->fk_user_action;	// owner of the action
				if (in_array($obj->code, array('TICKET_MSG', 'AC_TICKET_CREATE')) && empty($obj->fk_user_author)) {
					$this->cache_msgs_ticket[$i]['fk_contact_author'] = $obj->email_from;
				}
				$this->cache_msgs_ticket[$i]['datec'] = $this->db->jdate($obj->datec);
				$this->cache_msgs_ticket[$i]['datep'] = $this->db->jdate($obj->datep);
				$this->cache_msgs_ticket[$i]['subject'] = $obj->label;
				$this->cache_msgs_ticket[$i]['message'] = $obj->message;
				$this->cache_msgs_ticket[$i]['private'] = (preg_match('/^TICKET_MSG_PRIVATE/', $obj->code) ? 1 : 0);
				$i++;
			}
			return $num;
		} else {
			$this->error = "Error ".$this->db->lasterror();
			dol_syslog(get_class($this)."::load_cache_actions_ticket ".$this->error, LOG_ERR);
			return -1;
		}
	}

	/**
	 *    Close a ticket
	 *
	 *    @param    User    $user      	User that close
	 *    @param	int		$mode		0=Close solved, 1=Close abandoned
	 *    @return   int		           	Return integer <0 if KO, 0=nothing done, >0 if OK
	 */
	public function close(User $user, $mode = 0)
	{

		if ($this->status != Ticket::STATUS_CLOSED && $this->status != Ticket::STATUS_CANCELED) { // not closed
			$this->db->begin();

			$this->oldcopy = dol_clone($this);
			$this->status = ($mode ? Ticket::STATUS_CANCELED : Ticket::STATUS_CLOSED);

			$sql = "UPDATE ".MAIN_DB_PREFIX."ticket";
			$sql .= " SET fk_statut = ".((int) $this->status).", progress=100, date_close='".$this->db->idate(dol_now())."'";
			$sql .= " WHERE rowid = ".((int) $this->id);

			dol_syslog(get_class($this)."::close mode=".$mode);
			$resql = $this->db->query($sql);
			if ($resql) {
				$error = 0;

				// Valid and close fichinter linked
				if (isModEnabled('intervention') && getDolGlobalString('WORKFLOW_TICKET_CLOSE_INTERVENTION')) {
					dol_syslog("We have closed the ticket, so we close all linked interventions");
					$this->fetchObjectLinked($this->id, $this->element, null, 'fichinter');
					if ($this->linkedObjectsIds) {
						foreach ($this->linkedObjectsIds['fichinter'] as $fichinter_id) {
							$fichinter = new Fichinter($this->db);
							$fichinter->fetch($fichinter_id);
							if ($fichinter->statut == 0) {
								$result = $fichinter->setValid($user);
								if (!$result) {
									$this->errors[] = $fichinter->error;
									$error++;
								}
							}
							if ($fichinter->statut < 3) {
								$result = $fichinter->setStatut(3);
								if (!$result) {
									$this->errors[] = $fichinter->error;
									$error++;
								}
							}
						}
					}
				}

				// Call trigger
				$result = $this->call_trigger('TICKET_CLOSE', $user);
				if ($result < 0) {
					$error++;
				}
				// End call triggers

				if (!$error) {
					$this->db->commit();
					return 1;
				} else {
					$this->db->rollback();
					$this->error = implode(',', $this->errors);
					dol_syslog(get_class($this)."::close ".$this->error, LOG_ERR);
					return -1;
				}
			} else {
				$this->db->rollback();
				$this->error = $this->db->lasterror();
				dol_syslog(get_class($this)."::close ".$this->error, LOG_ERR);
				return -1;
			}
		}

		return 0;
	}

	/**
	 *     Search and fetch thirparties by email
	 *
	 *     @param  string 		$email   	Email
	 *     @param  int<0,3>		$type    	Type of thirdparties (0=any, 1=customer, 2=prospect, 3=supplier)
	 *     @param  array<string,mixed>	$filters 	Array of couple field name/value to filter the companies with the same name
	 *     @param  string 		$clause  	Clause for filters
	 *     @return Societe[]|int<-1,-1>		Array of thirdparties object
	 */
	public function searchSocidByEmail($email, $type = 0, $filters = array(), $clause = 'AND')
	{
		$thirdparties = array();
		$exact = 0;

		// Generation requete recherche
		$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."societe";
		$sql .= " WHERE entity IN (".getEntity('ticket', 1).")";
		if (!empty($type)) {
			if ($type == 1 || $type == 2) {
				$sql .= " AND client = ".((int) $type);
			} elseif ($type == 3) {
				$sql .= " AND fournisseur = 1";
			}
		}
		if (!empty($email)) {
			if (empty($exact)) {
				$regs = array();
				if (preg_match('/^([\*])?[^*]+([\*])?$/', $email, $regs) && count($regs) > 1) {
					$email = str_replace('*', '%', $email);
				} else {
					$email = '%'.$email.'%';
				}
			}
			$sql .= " AND ";
			if (is_array($filters) && !empty($filters)) {
				$sql .= "(";
			}

			$sql .= "email LIKE '".$this->db->escape($email)."'";
		}
		if (is_array($filters) && !empty($filters)) {
			foreach ($filters as $field => $value) {
				$sql .= " ".$clause." ".$this->db->sanitize($field)." LIKE '".$this->db->escape($value)."'";
			}
			if (!empty($email)) {
				$sql .= ")";
			}
		}

		$res = $this->db->query($sql);
		if ($res) {
			while ($rec = $this->db->fetch_array($res)) {
				$soc = new Societe($this->db);
				$soc->fetch($rec['rowid']);
				$thirdparties[] = $soc;
			}

			return $thirdparties;
		} else {
			$this->error = $this->db->error().' sql='.$sql;
			dol_syslog(get_class($this)."::searchSocidByEmail ".$this->error, LOG_ERR);
			return -1;
		}
	}

	/**
	 *     Search and fetch contacts by email
	 *
	 *     @param  string 		$email 		Email
	 *     @param  int  		$socid 		Limit to a thirdparty
	 *     @param  string 		$case  		Respect case
	 *     @return Contact[]|int        		Array of contacts object
	 */
	public function searchContactByEmail($email, $socid = 0, $case = '')
	{
		$contacts = array();

		// Forge the search SQL
		$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."socpeople";
		$sql .= " WHERE entity IN (".getEntity('contact').")";
		if (!empty($socid)) {
			$sql .= " AND fk_soc = ".((int) $socid);
		}
		if (!empty($email)) {
			$sql .= " AND ";
			if (!$case) {
				$sql .= "email = '".$this->db->escape($email)."'";
			} else {
				$sql .= "email LIKE BINARY '".$this->db->escape($this->db->escapeforlike($email))."'";
			}
		}

		$res = $this->db->query($sql);
		if ($res) {
			while ($rec = $this->db->fetch_object($res)) {
				include_once DOL_DOCUMENT_ROOT.'/contact/class/contact.class.php';
				$contactstatic = new Contact($this->db);
				$contactstatic->fetch($rec->rowid);
				$contacts[] = $contactstatic;
			}

			return $contacts;
		} else {
			$this->error = $this->db->error().' sql='.$sql;
			dol_syslog(get_class($this)."::searchContactByEmail ".$this->error, LOG_ERR);
			return -1;
		}
	}

	/**
	 *    Define parent commany of current ticket
	 *
	 *    @param  int $id		Id of thirdparty to set or '' to remove
	 *    @return int           Return integer <0 if KO, >0 if OK
	 */
	public function setCustomer($id)
	{
		if ($this->id) {
			$sql = "UPDATE ".MAIN_DB_PREFIX."ticket";
			$sql .= " SET fk_soc = ".($id > 0 ? (int) $id : "null");
			$sql .= " WHERE rowid = ".((int) $this->id);
			dol_syslog(get_class($this).'::setCustomer sql='.$sql);
			$resql = $this->db->query($sql);
			if ($resql) {
				return 1;
			} else {
				return -1;
			}
		} else {
			return -1;
		}
	}

	/**
	 *    Define progression of current ticket
	 *
	 *    @param  int $percent Progression percent
	 *    @return int             Return integer <0 if KO, >0 if OK
	 */
	public function setProgression($percent)
	{
		if ($this->id) {
			$sql = "UPDATE ".MAIN_DB_PREFIX."ticket";
			$sql .= " SET progress = ".($percent > 0 ? (float) $percent : "null");
			$sql .= " WHERE rowid = ".((int) $this->id);
			dol_syslog(get_class($this).'::set_progression sql='.$sql);
			$resql = $this->db->query($sql);
			if ($resql) {
				return 1;
			} else {
				return -1;
			}
		} else {
			return -1;
		}
	}

	/**
	 *     Link element with a contract
	 *
	 *     @param  int $contractid Contract id to link element to
	 *     @return int                        Return integer <0 if KO, >0 if OK
	 */
	public function setContract($contractid)
	{
		if ($this->id) {
			$sql = "UPDATE ".MAIN_DB_PREFIX."ticket";
			$sql .= " SET fk_contract = ".($contractid > 0 ? (int) $contractid : "null");
			$sql .= " WHERE rowid = ".((int) $this->id);
			dol_syslog(get_class($this).'::setContract sql='.$sql);
			$resql = $this->db->query($sql);
			if ($resql) {
				return 1;
			} else {
				return -1;
			}
		} else {
			return -1;
		}
	}

	/* gestion des contacts d'un ticket */

	/**
	 *  Return id des contacts interne de suivi
	 *
	 *  @return null|int[]       Liste des id contacts suivi ticket
	 */
	public function getIdTicketInternalContact()
	{
		return $this->getIdContact('internal', 'SUPPORTTEC');
	}

	/**
	 *  Retrieve information about internal contacts
	 *
	 *  @param    int     $status     Status of user or company                                array<array{id:int,email:string,firstname:string,lastname:string,libelle:string}>
	 *  @return array<array{id:int,email:string,firstname:string,lastname:string,libelle:string,socid:int,code:string,status:int}>             Array with datas : firstname, lastname, socid (-1 for internal users), email, code, libelle, status
	 */
	public function getInfosTicketInternalContact($status = -1)
	{
		return $this->listeContact(-1, 'internal', 0, '', $status);
	}

	/**
	 *  Return id des contacts clients pour le suivi ticket
	 *
	 *  @return null|int[]       Liste des id contacts suivi ticket
	 */
	public function getIdTicketCustomerContact()
	{
		return $this->getIdContact('external', 'SUPPORTCLI');
	}

	/**
	 * Retrieve information about external contacts
	 *
	 *  @param    int     $status     Status of user or company
	 *	@return   array<int|array{source:string,id:int,rowid:int,email:string,civility:string,firstname:string,lastname:string,labeltype:string,libelle:string,socid:int,code:string,status:int,statuscontact:int,fk_c_typecontact:int,phone:string,phone_mobile:string,phone_perso?:string,nom:string}>|int<-1,-1>      Array with data : firstname, lastname, socid (-1 for internal users), email, code, libelle, status
	 */
	public function getInfosTicketExternalContact($status = -1)
	{
		return $this->listeContact(-1, 'external', 0, '', $status);
	}

	/**
	 *  Return id des contacts clients des intervenants
	 *
	 *  @return null|int[]       Liste des id contacts intervenants
	 */
	public function getIdTicketInternalInvolvedContact()
	{
		return $this->getIdContact('internal', 'CONTRIBUTOR');
	}

	/**
	 *  Return id des contacts clients des intervenants
	 *
	 *  @return null|int[]       Liste des id contacts intervenants
	 */
	public function getIdTicketCustomerInvolvedContact()
	{
		return $this->getIdContact('external', 'CONTRIBUTOR');
	}

	/**
	 * Return id of all contacts for ticket
	 *
	 * @return	int[]		Array of contacts for tickets
	 */
	public function getTicketAllContacts()
	{
		$array_contact = $this->getIdTicketInternalContact();

		$array_contact = array_merge($array_contact, $this->getIdTicketCustomerContact());

		$array_contact = array_merge($array_contact, $this->getIdTicketInternalInvolvedContact());

		$array_contact = array_merge($array_contact, $this->getIdTicketCustomerInvolvedContact());

		return $array_contact;
	}

	/**
	 * Return id of all contacts for ticket
	 *
	 * @return	int[]		Array of contacts
	 */
	public function getTicketAllCustomerContacts()
	{
		$array_contact = array();

		$array_contact = array_merge($array_contact, $this->getIdTicketCustomerContact());

		$array_contact = array_merge($array_contact, $this->getIdTicketCustomerInvolvedContact());

		return $array_contact;
	}


	/**
	 *    Get array of all contacts for a ticket
	 *    Override method of file commonobject.class.php to add phone number
	 *
	 *    @param    int     $statusoflink   Status of lines to get (-1=all)
	 *    @param    string  $source         Source of contact: external or thirdparty (llx_socpeople) or internal (llx_user)
	 *    @param    int     $list           0:Return array contains all properties, 1:Return array contains just id
	 *    @param    string  $code           Filter on this code of contact type ('SHIPPING', 'BILLING', ...)
	 *    @param    int     $status         Status of user or company
	 *    @return   array<int|array{source:string,id:int,rowid:int,email:string,civility:string,firstname:string,lastname:string,labeltype:string,libelle:string,socid:int,code:string,status:int,statuscontact:int,fk_c_typecontact:int,phone:string,phone_mobile:string,phone_perso?:string,nom:string}>|int<-1,-1>      Array of array('email'=>..., 'lastname'=>...)
	 */
	public function listeContact($statusoflink = -1, $source = 'external', $list = 0, $code = '', $status = -1)
	{
		global $langs;

		$tab = array();

		$sql = "SELECT ec.rowid, ec.statut  as statuslink, ec.fk_socpeople as id, ec.fk_c_type_contact"; // This field contains id of llx_socpeople or id of llx_user
		if ($source == 'internal') {
			$sql .= ", '-1' as socid, t.statut as statuscontact";
		}

		if ($source == 'external' || $source == 'thirdparty') {
			$sql .= ", t.fk_soc as socid, t.statut as statuscontact";
		}

		$sql .= ", t.civility, t.lastname as lastname, t.firstname, t.email";
		if ($source == 'internal') {
			$sql .= ", t.office_phone as phone, t.user_mobile as phone_mobile";
		}

		if ($source == 'external') {
			$sql .= ", t.phone as phone, t.phone_mobile as phone_mobile, t.phone_perso as phone_perso";
		}

		$sql .= ", tc.source, tc.element, tc.code, tc.libelle as type_contact_label";
		$sql .= " FROM ".MAIN_DB_PREFIX."c_type_contact tc";
		$sql .= ", ".MAIN_DB_PREFIX."element_contact ec";
		if ($source == 'internal') {
			$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user t on ec.fk_socpeople = t.rowid";
		}

		if ($source == 'external' || $source == 'thirdparty') {
			$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."socpeople t on ec.fk_socpeople = t.rowid";
		}

		$sql .= " WHERE ec.element_id = ".((int) $this->id);
		$sql .= " AND ec.fk_c_type_contact=tc.rowid";
		$sql .= " AND tc.element='".$this->db->escape($this->element)."'";
		if ($source == 'internal') {
			$sql .= " AND tc.source = 'internal'";
			if ($status >= 0) {
				$sql .= " AND t.statut = ".((int) $status);
			}
		}

		if ($source == 'external' || $source == 'thirdparty') {
			$sql .= " AND tc.source = 'external'";
			if ($status >= 0) {
				$sql .= " AND t.statut = ".((int) $status);
			}
		}

		if (!empty($code)) {
			$sql .= " AND tc.code = '".$this->db->escape($code)."'";
		}

		$sql .= " AND tc.active=1";
		if ($statusoflink >= 0) {
			$sql .= " AND ec.statut = ".((int) $statusoflink);
		}

		$sql .= " ORDER BY t.lastname ASC";

		$resql = $this->db->query($sql);
		if ($resql) {
			$num = $this->db->num_rows($resql);
			$i = 0;
			while ($i < $num) {
				$obj = $this->db->fetch_object($resql);

				if (!$list) {
					$transkey = "TypeContact_".$obj->element."_".$obj->source."_".$obj->code;
					$labelType = ($langs->trans($transkey) != $transkey ? $langs->trans($transkey) : $obj->type_contact_label);
					$tab[$i] = array(
						'source' => $obj->source,
						'socid' => (int) $obj->socid,
						'id' => (int) $obj->id,
						'nom' => $obj->lastname, // For backward compatibility
						'civility' => $obj->civility,
						'lastname' => $obj->lastname,
						'firstname' => $obj->firstname,
						'email' => $obj->email,
						'rowid' => (int) $obj->rowid,
						'code' => $obj->code,
						'libelle' => $labelType,		// deprecated, replaced with labeltype
						'labeltype' => $labelType,
						'status' => $obj->statuslink,
						'statuscontact' => (int) $obj->statuscontact,
						'fk_c_type_contact' => (int) $obj->fk_c_type_contact,
						'phone' => $obj->phone,
						'phone_mobile' => $obj->phone_mobile);
					if ($source == 'external') {
						$tab[$i]['phone_perso'] = $obj->phone_perso;
					}
				} else {
					$tab[$i] = $obj->id;
				}

				$i++;
			}

			return $tab;
		} else {
			$this->error = $this->db->error();
			dol_print_error($this->db);
			return -1;
		}
	}

	/**
	 * Get a default reference.
	 *
	 * @param	Societe		$thirdparty		Thirdparty
	 * @return 	string   					Reference
	 */
	public function getDefaultRef($thirdparty = null)
	{
		global $conf;

		$defaultref = '';
		$modele = getDolGlobalString('TICKET_ADDON', 'mod_ticket_simple');

		// Search template files
		$file = '';
		$classname = '';
		$reldir = '';
		$dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
		foreach ($dirmodels as $reldir) {
			$file = dol_buildpath($reldir."core/modules/ticket/".$modele.'.php', 0);
			if (file_exists($file)) {
				$classname = $modele;
				break;
			}
		}

		if ($classname !== '') {
			$result = dol_include_once($reldir."core/modules/ticket/".$modele.'.php');
			$modTicket = new $classname();
			'@phan-var-force ModeleNumRefTicket $modTicket';

			$defaultref = $modTicket->getNextValue($thirdparty, $this);
		}

		if (is_numeric($defaultref) && $defaultref <= 0) {
			$defaultref = '';
		}

		return $defaultref;
	}


	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
	/**
	 *  Return if at least one photo is available
	 *
	 *  @param      string      $sdir       Directory to scan
	 *  @return     boolean                 True if at least one photo is available, False if not
	 */
	public function is_photo_available($sdir)
	{
		// phpcs:enable
		include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';

		$dir = $sdir.'/';

		$dir_osencoded = dol_osencode($dir);
		if (file_exists($dir_osencoded)) {
			$handle = opendir($dir_osencoded);
			if (is_resource($handle)) {
				while (($file = readdir($handle)) !== false) {
					if (!utf8_check($file)) {
						$file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1');	// To be sure data is stored in UTF8 in memory
					}
					if (dol_is_file($dir.$file)) {
						return true;
					}
				}
			}
		}
		return false;
	}


	/**
	 * Copy files defined into $_SESSION array into the ticket directory of attached files.
	 * Used for files linked into messages.
	 * Files may be renamed during copy to avoid overwriting existing files.
	 *
	 * @param	string		$forcetrackid	Force trackid used for $keytoavoidconflict into get_attached_files()
	 * @return	array{listofpaths:string[],listofnames:string[],listofmimes:string[]}|int<-1,-1>	Array with final path/name/mime of files.
	 */
	public function copyFilesForTicket($forcetrackid = null)
	{
		global $conf;

		// Create form object
		include_once DOL_DOCUMENT_ROOT.'/core/class/html.formmail.class.php';
		include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
		include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';

		$maxwidthsmall = 270;
		$maxheightsmall = 150;
		$maxwidthmini = 128;
		$maxheightmini = 72;

		$formmail = new FormMail($this->db);
		$formmail->trackid = (is_null($forcetrackid) ? 'tic'.$this->id : '');
		$attachedfiles = $formmail->get_attached_files();

		$filepath = $attachedfiles['paths'];	// path is for example user->dir_temp.'/'.$user->id.'/'...
		$filename = $attachedfiles['names'];
		$mimetype = $attachedfiles['mimes'];

		// Copy files into ticket directory
		$destdir = $conf->ticket->dir_output.'/'.$this->ref;

		if (!dol_is_dir($destdir)) {
			dol_mkdir($destdir);
		}

		$listofpaths = array();
		$listofnames = array();
		foreach ($filename as $i => $val) {
			$destfile = $destdir.'/'.$filename[$i];
			// If destination file already exists, we add a suffix to avoid to overwrite
			if (is_file($destfile)) {
				$pathinfo = pathinfo($filename[$i]);
				$now = dol_now();
				$destfile = $destdir.'/'.$pathinfo['filename'].' - '.dol_print_date($now, 'dayhourlog').'.'.$pathinfo['extension'];
			}

			$moreinfo = array(
				'description' => 'File saved by copyFilesForTicket',
				'src_object_type' => $this->element,
				'src_object_id' => $this->id,
				'gen_or_uploaded' => 'uploaded'
			);

			$res = dol_move($filepath[$i], $destfile, '0', 1, 0, 1, $moreinfo);

			if (!$res) {
				// Move has failed
				$this->error = "Failed to move file ".dirbasename($filepath[$i])." into ".dirbasename($destfile);
				return -1;
			} else {
				// If file is an image, we create thumbs
				if (image_format_supported($destfile) == 1) {
					// Create small thumbs for image (Ratio is near 16/9)
					// Used on logon for example
					$imgThumbSmall = vignette($destfile, $maxwidthsmall, $maxheightsmall, '_small', 50, "thumbs");
					// Create mini thumbs for image (Ratio is near 16/9)
					// Used on menu or for setup page for example
					$imgThumbMini = vignette($destfile, $maxwidthmini, $maxheightmini, '_mini', 50, "thumbs");
				}
			}

			// Clear variables into session
			$formmail->remove_attached_files($i);

			// Fill array with new names
			$listofpaths[$i] = $destfile;
			$listofnames[$i] = basename($destfile);
		}

		return array('listofpaths' => $listofpaths, 'listofnames' => $listofnames, 'listofmimes' => $mimetype);
	}

	/**
	 * Sets object to supplied categories.
	 *
	 * Deletes object from existing categories not supplied.
	 * Adds it to non existing supplied categories.
	 * Existing categories are left untouch.
	 *
	 * @param  int[]|int 	$categories 	Category or categories IDs
	 * @return int							Return integer <0 if KO, >0 if OK
	 */
	public function setCategories($categories)
	{
		// Handle single category
		if (!is_array($categories)) {
			$categories = array($categories);
		}

		// Get current categories
		include_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
		$c = new Categorie($this->db);
		$existing = $c->containing($this->id, Categorie::TYPE_TICKET, 'id');

		// Diff
		if (is_array($existing)) {
			$to_del = array_diff($existing, $categories);
			$to_add = array_diff($categories, $existing);
		} else {
			$to_del = array(); // Nothing to delete
			$to_add = $categories;
		}

		// Process
		foreach ($to_del as $del) {
			if ($c->fetch($del) > 0) {
				$c->del_type($this, Categorie::TYPE_TICKET);
			}
		}
		foreach ($to_add as $add) {
			if ($c->fetch($add) > 0) {
				$c->add_type($this, Categorie::TYPE_TICKET);
			}
		}

		return 1;
	}

	/**
	 * Add new message on a ticket (private/public area).
	 * Can also send it by email if GETPOST('send_email', 'int') is set. For such email, header and footer is added.
	 *
	 * @param   User    $user       	User for action
	 * @param   string  $action     	Action string
	 * @param   int     $private    	1=Message is private (must not be visible by external users)
	 * @param   int     $public_area    0=Default,
	 * 									1=If we are creating the message from a public area, so confirmation email will be sent to the author
	 * 									and we can search contact from email to add it as contact of ticket if TICKET_ASSIGN_CONTACT_TO_MESSAGE is set
	 * @return  int						Return integer <0 if KO, >= 0 if OK
	 */
	public function newMessage($user, &$action, $private = 1, $public_area = 0)
	{
		global $mysoc, $langs, $hookmanager;

		$error = 0;

		$object = new Ticket($this->db);

		$ret = $object->fetch(0, '', GETPOST('track_id', 'alpha'));

		$object->socid = $object->fk_soc;
		$object->fetch_thirdparty();
		$object->fetchProject();

		if ($ret < 0) {
			$error++;
			array_push($this->errors, $langs->trans("ErrorTicketIsNotValid"));
			$action = '';
		}

		if (!GETPOST("message")) {
			$error++;
			array_push($this->errors, $langs->trans("ErrorFieldRequired", $langs->transnoentities("Message")));
			$action = 'add_message';
		}

		if (!$error) {
			$summary = mb_strcut(GETPOST('summary', 'alphanohtml'), 0, 42, 'UTF-8');
			$object->subject = GETPOST('subject', 'alphanohtml');
			$object->message = GETPOST("message", "restricthtml");
			$object->private = GETPOST("private_message", "alpha");

			$send_email = (bool) GETPOSTINT('send_email');

			// Copy attached files (saved into $_SESSION) as linked files to ticket. Return array with final name used.
			$resarray = $object->copyFilesForTicket();
			if (is_numeric($resarray) && $resarray == -1) {
				setEventMessages($object->error, $object->errors, 'errors');
				return -1;
			}

			$listofpaths = $resarray['listofpaths'];
			$listofnames = $resarray['listofnames'];
			$listofmimes = $resarray['listofmimes'];

			// Retrieve email of all contacts (external)
			$external_contacts = $object->getInfosTicketExternalContact(1);
			$external_resources = [];
			if (!empty($external_contacts)) {
				foreach ($external_contacts as $eContact) {
					$external_resources[$eContact['id']] = $eContact;
				}
			}

			// Add the ticket message in database (even if email is requested, we store a simple record
			// like a simple private message, with no information about emails) because
			// information about emails sent will be fill later after email sending.
			$id = $object->createTicketMessage($user, 0, $listofpaths, $listofmimes, $listofnames, $send_email, $public_area, $summary, $external_resources);

			if ($id <= 0) {
				$error++;
				$this->error = $object->error;
				$this->errors = $object->errors;
				$action = 'add_message';
			}

			if (!$error && $id > 0) {
				setEventMessages($langs->trans('TicketMessageSuccessfullyAdded'), null, 'mesgs');

				// Now send emails
				if (!empty($public_area)) {
					/*
					 * Message created from the Public interface
					 *
					 * Send emails to assigned users (public area notification)
					 */
					if (getDolGlobalString('TICKET_PUBLIC_NOTIFICATION_NEW_MESSAGE_ENABLED')) {
						// Retrieve internal contact datas
						$internal_contacts = $object->getInfosTicketInternalContact(1);

						$assigned_user_dont_have_email = '';

						$sendto = array();

						if ($this->fk_user_assign > 0) {
							$assigned_user = new User($this->db);
							$assigned_user->fetch($this->fk_user_assign);
							if (!empty($assigned_user->email)) {
								$sendto[$assigned_user->email] = $assigned_user->getFullName($langs)." <".$assigned_user->email.">";
							} else {
								$assigned_user_dont_have_email = $assigned_user->getFullName($langs);
							}
						} else {
							$assigned_user = null;
						}

						// Build array to display recipient list
						foreach ($internal_contacts as $key => $info_sendto) {
							// Avoid duplicate notifications
							if ($info_sendto['id'] == $user->id) {
								continue;
							}

							// We check if the email address is not the assignee's address to prevent notification from being sent twice
							if (!empty($info_sendto['email']) && ($assigned_user === null || $assigned_user->email != $info_sendto['email'])) {
								$sendto[] = dolGetFirstLastname($info_sendto['firstname'], $info_sendto['lastname'])." <".$info_sendto['email'].">";
							}
						}

						if (empty($sendto)) {
							if (getDolGlobalString('TICKET_PUBLIC_NOTIFICATION_NEW_MESSAGE_DEFAULT_EMAIL')) {
								$sendto[getDolGlobalString('TICKET_PUBLIC_NOTIFICATION_NEW_MESSAGE_DEFAULT_EMAIL')] = getDolGlobalString('TICKET_PUBLIC_NOTIFICATION_NEW_MESSAGE_DEFAULT_EMAIL');
							} elseif (getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO')) {
								$sendto[getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO')] = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO');
							}
						}

						// Add global email address recipient
						if (getDolGlobalString('TICKET_NOTIFICATION_ALSO_MAIN_ADDRESS') && !array_key_exists(getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO'), $sendto)) {
							if (getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO')) {
								$sendto[getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO')] = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO');
							}
						}

						$parameters = array('sendto' => $sendto);
						$reshook = $hookmanager->executeHooks('updateSendtoTicketMessage', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
						if (empty($reshook)) {
							$sendto = array_merge($sendto, $hookmanager->resArray);
						} elseif ($reshook > 0) {
							$sendto = $hookmanager->resArray;
						}

						if (!empty($sendto)) {
							$appli = getDolGlobalString('MAIN_APPLICATION_TITLE', $mysoc->name);

							$subject = '['.$appli.'- ticket #'.$object->track_id.'] '.$this->subject;

							// Message send
							$message = getDolGlobalString('TICKET_MESSAGE_MAIL_INTRO', $langs->trans('TicketMessageMailIntroText'));
							$message .= '<br><br>';
							$messagePost = GETPOST('message', 'restricthtml');
							if (!dol_textishtml($messagePost)) {
								$messagePost = dol_nl2br($messagePost);
							}
							$message .= $messagePost;

							// Customer company infos
							$message .= '<br><br>';
							$message .= "==============================================";
							$message .= !empty($object->thirdparty->name) ? '<br>'.$langs->trans('ThirdParty')." : ".$object->thirdparty->name : '';
							$message .= !empty($object->thirdparty->town) ? '<br>'.$langs->trans('Town')." : ".$object->thirdparty->town : '';
							$message .= !empty($object->thirdparty->phone) ? '<br>'.$langs->trans('Phone')." : ".$object->thirdparty->phone : '';

							// Email send to
							$message .= '<br><br>';
							if (!empty($assigned_user_dont_have_email)) {
								$message .= '<br>'.$langs->trans('NoEMail').' : '.$assigned_user_dont_have_email;
							}
							foreach ($sendto as $val) {
								$message .= '<br>'.$langs->trans('TicketNotificationRecipient').' : '.$val;
							}

							// URL ticket
							$url_internal_ticket = dol_buildpath('/ticket/card.php', 2).'?track_id='.$object->track_id;
							$message .= '<br><br>';
							$message .= $langs->trans('TicketNotificationEmailBodyInfosTrackUrlinternal').' : <a href="'.$url_internal_ticket.'">'.$object->track_id.'</a>';

							$from = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_FROM');

							$replyto = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_REPLYTO');

							// don't try to send email if no recipient
							$this->sendTicketMessageByEmail($subject, $message, 0, $sendto, $listofpaths, $listofmimes, $listofnames, array(), $from, $replyto);
						}
					}
				} else {
					/*
					 * Message created from the Backoffice / Private area
					 *
					 * Send emails to internal users (linked contacts) then, if private is not set, to external users (linked contacts or thirdparty email if no contact set)
					 */
					if ((int) $send_email > 0) {
						// Retrieve internal contact datas
						$internal_contacts = $object->getInfosTicketInternalContact(1);

						$sendto = array();
						if (is_array($internal_contacts) && count($internal_contacts) > 0) {
							// Set default subject
							$appli = getDolGlobalString('MAIN_APPLICATION_TITLE', $mysoc->name);

							$subject = GETPOST('subject', 'alphanohtml') ? GETPOST('subject', 'alphanohtml') : '['.$appli.' - '.$langs->trans("Ticket").' #'.$object->track_id.'] '.$langs->trans('TicketNewMessage');

							$message_intro = $langs->trans('TicketNotificationEmailBody', "#".$object->id);
							$message_signature = GETPOST('mail_signature') ? GETPOST('mail_signature') : getDolGlobalString('TICKET_MESSAGE_MAIL_SIGNATURE');

							$message = getDolGlobalString('TICKET_MESSAGE_MAIL_INTRO', $langs->trans('TicketMessageMailIntroText'));
							$message .= '<br><br>';
							$messagePost = GETPOST('message', 'restricthtml');
							if (!dol_textishtml($messagePost)) {
								$messagePost = dol_nl2br($messagePost);
							}
							$message .= $messagePost;

							// Data about customer
							$message .= '<br><br>';
							$message .= "==============================================<br>";
							$message .= !empty($object->thirdparty->name) ? $langs->trans('ThirdParty')." : ".$object->thirdparty->name : '';
							$message .= !empty($object->thirdparty->town) ? '<br>'.$langs->trans('Town')." : ".$object->thirdparty->town : '';
							$message .= !empty($object->thirdparty->phone) ? '<br>'.$langs->trans('Phone')." : ".$object->thirdparty->phone : '';

							// Build array to display recipient list
							foreach ($internal_contacts as $key => $info_sendto) {
								// Check if recipient email is current user, if yes, we avoid to send email to him.
								if ($info_sendto['id'] == $user->id) {
									dol_syslog("We cancel sending email to internal user ".$info_sendto['email']." because it is current user", LOG_DEBUG);
									continue;
								}

								if ($info_sendto['email'] != '') {
									$email = $info_sendto['email'];
									if ($email != null) {
										$sendto[$email] = dolGetFirstLastname($info_sendto['firstname'], $info_sendto['lastname'])." <".$info_sendto['email'].">";
									}

									// Contact type
									$recipient = dolGetFirstLastname($info_sendto['firstname'], $info_sendto['lastname'], -1).' ('.strtolower((string) $info_sendto['libelle']).')';
									$message .= (!empty($recipient) ? '<br>'.$langs->trans('TicketNotificationRecipient').' : '.$recipient.'<br>' : '');
								}
							}
							$message .= '<br>';
							// URL ticket
							$url_internal_ticket = dol_buildpath('/ticket/card.php', 2).'?track_id='.$object->track_id;

							// Add html link on url
							$message .= '<br>'.$langs->trans('TicketNotificationEmailBodyInfosTrackUrlinternal').' : <a href="'.$url_internal_ticket.'">'.$object->track_id.'</a><br>';

							// Add global email address recipient
							if (getDolGlobalString('TICKET_NOTIFICATION_ALSO_MAIN_ADDRESS') && !array_key_exists(getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO'), $sendto)) {
								if (getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO')) {
									$sendto[getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO')] = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO');
								}
							}

							$parameters = array('sendto' => $sendto);
							$reshook = $hookmanager->executeHooks('updateSendtoTicketMessage', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
							if (empty($reshook)) {
								$sendto = array_merge($sendto, $hookmanager->resArray);
							} elseif ($reshook > 0) {
								$sendto = $hookmanager->resArray;
							}

							$sendtocc = array();
							if (getDolGlobalString("TICKET_SEND_INTERNAL_CC")) {
								$sendtocc = explode(',', getDolGlobalString("TICKET_SEND_INTERNAL_CC"));
							}

							$from = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_FROM');

							$replyto = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_REPLYTO');

							// don't try to send email if no recipient
							if (!empty($sendto)) {
								$this->sendTicketMessageByEmail($subject, $message, 0, $sendto, $listofpaths, $listofmimes, $listofnames, $sendtocc, $from, $replyto);
							}
						}

						/*
						 * Send emails for externals users if not private (linked contacts)
						 */
						if (empty($object->private)) {
							// If no contact, get email from thirdparty
							if (is_array($external_contacts) && count($external_contacts) === 0) {
								if (!empty($object->fk_soc)) {
									$object->fetch_thirdparty($object->fk_soc);
									$array_company = array(array('firstname' => '', 'lastname' => $object->thirdparty->name, 'email' => $object->thirdparty->email, 'libelle' => $langs->transnoentities('Customer'), 'socid' => $object->thirdparty->id));
									$external_contacts = array_merge($external_contacts, $array_company);
								} elseif (empty($object->fk_soc) && !empty($object->origin_replyto)) {
									$array_external = array(array('firstname' => '', 'lastname' => $object->origin_replyto, 'email' => $object->origin_replyto, 'libelle' => $langs->transnoentities('Customer'), 'socid' => 0));
									$external_contacts = array_merge($external_contacts, $array_external);
								} elseif (empty($object->fk_soc) && !empty($object->origin_email)) {
									$array_external = array(array('firstname' => '', 'lastname' => $object->origin_email, 'email' => $object->thirdparty->email, 'libelle' => $langs->transnoentities('Customer'), 'socid' => $object->thirdparty->id));
									$external_contacts = array_merge($external_contacts, $array_external);
								}
							}

							$sendto = array();
							if (is_array($external_contacts) && count($external_contacts) > 0) {
								// Get default subject for email to external contacts
								$appli = getDolGlobalString('MAIN_APPLICATION_TITLE', $mysoc->name);

								$subject = GETPOST('subject') ? GETPOST('subject') : '['.$appli.' - '.$langs->trans("Ticket").' #'.$object->track_id.'] '.$langs->trans('TicketNewMessage');

								$message_intro = GETPOST('mail_intro') ? GETPOST('mail_intro', 'restricthtml') : getDolGlobalString('TICKET_MESSAGE_MAIL_INTRO');
								$message_signature = GETPOST('mail_signature') ? GETPOST('mail_signature', 'restricthtml') : getDolGlobalString('TICKET_MESSAGE_MAIL_SIGNATURE');
								if (!dol_textishtml($message_intro)) {
									$message_intro = dol_nl2br($message_intro);
								}
								if (!dol_textishtml($message_signature)) {
									$message_signature = dol_nl2br($message_signature);
								}

								// We put intro after
								$messagePost = GETPOST('message', 'restricthtml');
								if (!dol_textishtml($messagePost)) {
									$messagePost = dol_nl2br($messagePost);
								}
								$message = $messagePost;
								$message .= '<br><br>';

								foreach ($external_contacts as $key => $info_sendto) {
									// Check if recipient email is current user, if yes, we avoid to send email to him.
									if ($info_sendto['id'] == $user->contact_id) {
										continue;
									}

									if ($info_sendto['email'] != '' && $info_sendto['email'] != $object->origin_email) {
										$email = $info_sendto['email'];
										if ($email != null) {
											$sendto[$email] = trim($info_sendto['firstname']." ".$info_sendto['lastname'])." <".$info_sendto['email'].">";
										}

										$recipient = dolGetFirstLastname($info_sendto['firstname'], $info_sendto['lastname'], -1).' ('.strtolower((string) $info_sendto['libelle']).')';
										$message .= (!empty($recipient) ? '<br>'.$langs->trans('TicketNotificationRecipient').' : '.$recipient.'<br>' : '');
									}
								}

								// If public interface is not enable, use link to internal page into mail
								$url_public_ticket = (getDolGlobalInt('TICKET_ENABLE_PUBLIC_INTERFACE') ?
										(getDolGlobalString('TICKET_URL_PUBLIC_INTERFACE') ? getDolGlobalString('TICKET_URL_PUBLIC_INTERFACE') . '/view.php' : dol_buildpath('/public/ticket/view.php', 2)) : dol_buildpath('/ticket/card.php', 2)).'?track_id='.urlencode($object->track_id);

								if (getDolGlobalInt('TICKET_INCLUDE_LINK_TO_PUBLIC_INTERFACE_IN_MESSAGE')) {
									$message .= '<br>' . $langs->trans('TicketNewEmailBodyInfosTrackUrlCustomer') . ' : <a href="' . $url_public_ticket . '">' . $object->track_id . '</a><br>';
								}

								// Build final message
								$message = $message_intro.'<br><br>'.$message;

								// Add signature
								$message .= '<br>'.$message_signature;

								if (!empty($object->origin_replyto)) {
									$sendto[$object->origin_replyto] = $object->origin_replyto;
								} elseif (!empty($object->origin_email)) {
									$sendto[$object->origin_email] = $object->origin_email;
								}

								if ($object->fk_soc > 0 && !array_key_exists($object->origin_replyto, $sendto) && !array_key_exists($object->origin_email, $sendto)) {
									$object->socid = $object->fk_soc;
									$object->fetch_thirdparty();
									if (!empty($object->thirdparty->email)) {
										$sendto[$object->thirdparty->email] = $object->thirdparty->email;
									}
								}

								// Add global email address recipient
								if (getDolGlobalString('TICKET_NOTIFICATION_ALSO_MAIN_ADDRESS') && !array_key_exists(getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO'), $sendto)) {
									if (getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO')) {
										$sendto[getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO')] = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_TO');
									}
								}

								$parameters = array('sendto' => $sendto);
								$reshook = $hookmanager->executeHooks('updateSendtoTicketMessage', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
								if (empty($reshook)) {
									$sendto = array_merge($sendto, $hookmanager->resArray);
								} elseif ($reshook > 0) {
									$sendto = $hookmanager->resArray;
								}

								$sendtocc = array();
								if (getDolGlobalString("TICKET_SEND_INTERNAL_CC")) {
									$sendtocc = explode(',', getDolGlobalString("TICKET_SEND_INTERNAL_CC"));
								}

								// Don't try to send email when no recipient
								if (!empty($sendto)) {
									$from = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_FROM');

									$replyto = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_REPLYTO');

									$result = $this->sendTicketMessageByEmail($subject, $message, 0, $sendto, $listofpaths, $listofmimes, $listofnames, $sendtocc, $from, $replyto);
									if ($result) {
										// update last_msg_sent date of ticket (for last message sent to external users)
										$this->date_last_msg_sent = dol_now();
										$this->update($user, 1);	// disable trigger when updating date_last_msg_sent. sendTicketMessageByEmail already create an event in actioncomm table.

										// update event actioncomm $id
										//print 'update actioncomm id='.$id.' with sendto='.json_encode($sendto)." sendtocc=".json_encode($sendtocc)." email_msgid=".json_encode($this->email_msgid);
										$sql = "UPDATE ".MAIN_DB_PREFIX."actioncomm";
										$sql .= " SET email_msgid = '".$this->db->escape($this->email_msgid)."',";
										$sql .= " email_subject = '".$this->db->escape($subject)."',";
										$sql .= " email_from = '".$this->db->escape($from)."',";
										$sql .= " email_to = '".$this->db->escape(implode(',', $sendto))."',";
										$sql .= " email_tocc = '".$this->db->escape(implode(',', $sendtocc))."',";
										$sql .= " reply_to = '".$this->db->escape($replyto)."'";
										$sql .= " WHERE id = ".((int) $id);

										$resql = $this->db->query($sql);
									}
								}
							}
						}
					}
				}

				// Set status back to "In progress" if not set yet, but only if internal user and not a private message
				// Or set status to "In progress" if the client has answered and if the ticket has started
				// So we are sure to leave the STATUS_DRAFT, STATUS_NEED_INFO.
				// Except if TICKET_SET_STATUS_ON_ANSWER has been defined
				if ((getDolGlobalInt('TICKET_SET_STATUS_ON_ANSWER', -1) < 0
				&& ($object->status < self::STATUS_IN_PROGRESS && !$user->socid && !$private))
				|| ($object->status > self::STATUS_IN_PROGRESS && $public_area)) {
					// Set status
					$object->setStatut($object::STATUS_IN_PROGRESS, null, '', 'TICKET_MODIFY');
				} elseif (getDolGlobalInt('TICKET_SET_STATUS_ON_ANSWER', -1) >= 0 && empty($user->socid) && empty($private)) {
					// Set status
					$object->setStatut(getDolGlobalInt('TICKET_SET_STATUS_ON_ANSWER'), null, '', 'TICKET_MODIFY');
				}

				return 1;
			} else {
				setEventMessages($object->error, $object->errors, 'errors');
				return -1;
			}
		} else {
			setEventMessages($this->error, $this->errors, 'errors');
			return -1;
		}
	}


	/**
	 * Send ticket by email to linked contacts
	 *
	 * @param string		$subject          	Email subject
	 * @param string		$message          	Email message
	 * @param int<0,1>		$send_internal_cc 	Receive a copy on internal email at getDolGlobalString('TICKET_NOTIFICATION_EMAIL_FROM')
	 * @param array<string>	$array_receiver		Array of receiver. Example array('name' => 'John Doe', 'email' => 'john@doe.com', etc...)
	 * @param string[]		$filename_list      List of files to attach (full path of filename on file system)
	 * @param string[]		$mimetype_list      List of MIME type of attached files
	 * @param string[]		$mimefilename_list  List of attached file name in message
	 * @param array<string>	$array_receiver_cc	Array of receiver in CC. Example array('john@doe.com')
	 * @param string		$from				Email from
	 * @param string		$replyto			Reply to
	 * @return boolean     						True if mail sent to at least one receiver, false otherwise
	 */
	public function sendTicketMessageByEmail($subject, $message, $send_internal_cc = 0, $array_receiver = array(), $filename_list = array(), $mimetype_list = array(), $mimefilename_list = array(), $array_receiver_cc = array(), $from = '', $replyto = '')
	{
		global $conf, $langs, $user, $hookmanager;

		if (getDolGlobalString('TICKET_DISABLE_ALL_MAILS')) {
			dol_syslog(get_class($this).'::sendTicketMessageByEmail: Emails are disable into ticket setup by option TICKET_DISABLE_ALL_MAILS', LOG_WARNING);
			return false;
		}

		$langs->load("mails");

		include_once DOL_DOCUMENT_ROOT.'/contact/class/contact.class.php';
		//$contactstatic = new Contact($this->db);

		// If no receiver defined, load all ticket linked contacts
		if (!is_array($array_receiver) || !count($array_receiver) > 0) {
			$array_receiver = $this->getInfosTicketInternalContact(1);
			$array_receiver = array_merge($array_receiver, $this->getInfosTicketExternalContact(1));
		}

		dol_syslog("sendTicketMessageByEmail array_receiver=".json_encode($array_receiver), LOG_DEBUG);
		dol_syslog("sendTicketMessageByEmail array_receiver_cc=".json_encode($array_receiver_cc), LOG_DEBUG);

		$sendtocc = '';
		if ($send_internal_cc) {
			$sendtocc = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_FROM');
		}
		if (!empty($array_receiver_cc) && is_array($array_receiver_cc)) {
			$sendtocc .= ($sendtocc ? ',' : '').implode(',', $array_receiver_cc);
		}

		if (empty($from)) {
			$from = getDolGlobalString('TICKET_NOTIFICATION_EMAIL_FROM');
		}

		$parameters = array('from' => $from);
		$action = '';
		$reshook = $hookmanager->executeHooks('getTicketMessageEmailFrom', $parameters, $this, $action);
		if ($reshook && !empty($hookmanager->resArray['from'])) {
			$from = $hookmanager->resArray['from'];
		}

		$is_sent = false;
		$this->email_msgids = array();

		if (is_array($array_receiver) && count($array_receiver) > 0) {
			//foreach ($array_receiver as $key => $receiver) {
			$deliveryreceipt = 0;
			$filepath = $filename_list;
			$filename = $mimefilename_list;
			$mimetype = $mimetype_list;

			// Send email

			$old_MAIN_MAIL_AUTOCOPY_TO = getDolGlobalString('MAIN_MAIL_AUTOCOPY_TO');
			if (getDolGlobalString('TICKET_DISABLE_MAIL_AUTOCOPY_TO')) {
				$conf->global->MAIN_MAIL_AUTOCOPY_TO = '';
			}

			$upload_dir_tmp = $conf->user->dir_output."/".$user->id.'/temp';

			include_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
			$trackid = "tic".$this->id;

			$moreinheader = 'X-Dolibarr-Info: sendTicketMessageByEmail'."\r\n";
			if (!empty($this->email_msgid)) {
				// We must also add 1 entry In-Reply-To: <$this->email_msgid> with Message-ID we respond from (See RFC5322).
				$moreinheader .= 'In-Reply-To: <'.$this->email_msgid.'>'."\r\n";
				// TODO We should now be able to give the in_reply_to as a dedicated parameter of new CMailFile() instead of into $moreinheader.
			}

			// We should add here also a header 'References:'
			// According to RFC5322, we should add here all the References fields of the initial message concatenated with
			// the Message-ID of the message we respond from (but each ID must be once).
			$references = '';
			if (!empty($this->origin_references)) {		// $this->origin_references should be '<'.$this->origin_references.'>'
				$references .= (empty($references) ? '' : ' ').$this->origin_references;
			}
			if (!empty($this->email_msgid) && !preg_match('/'.preg_quote($this->email_msgid, '/').'/', $references)) {
				$references .= (empty($references) ? '' : ' ').'<'.$this->email_msgid.'>';
			}
			if ($references) {
				$moreinheader .= 'References: '.$references."\r\n";
				// TODO We should now be able to give the references as a dedicated parameter of new CMailFile() instead of into $moreinheader.
			}

			$receiverstring = '';
			foreach ($array_receiver as $key => $receiver) {
				$receiverstring .= ($receiverstring ? ',' : '').$receiver;
			}

			$sendcontext = 'ticket';

			// Send email
			$mailfile = new CMailFile($subject, $receiverstring, $from, $message, $filepath, $mimetype, $filename, $sendtocc, '', $deliveryreceipt, -1, '', '', $trackid, $moreinheader, $sendcontext, $replyto, $upload_dir_tmp);


			if ($mailfile->error) {
				setEventMessages($mailfile->error, null, 'errors');
			} else {
				$result = $mailfile->sendfile();

				if ($result) {
					setEventMessages($langs->trans('MailSuccessfulySent', $mailfile->getValidAddress($from, 2), $mailfile->getValidAddress($receiverstring, 2)), null, 'mesgs');
					$is_sent = true;

					$this->email_msgid = $mailfile->msgid;
				} else {
					$langs->load("other");
					if ($mailfile->error) {
						setEventMessages($langs->trans('ErrorFailedToSendMail', $from, $receiverstring), null, 'errors');
						dol_syslog('ErrorFailedToSendMail from='.$from.' to='.$receiverstring.' : '.$mailfile->error);
					} elseif (getDolGlobalString('MAIN_DISABLE_ALL_MAILS')) {
						setEventMessages('No mail sent. Feature is disabled by option MAIN_DISABLE_ALL_MAILS', null, 'errors');
					} else {
						setEventMessages($langs->trans('ErrorFailedToSendMail', $from, $receiverstring), null, 'errors');
						dol_syslog('ErrorFailedToSendMail from='.$from.' to='.$receiverstring.' : (no error details)', LOG_WARNING);
					}
				}
			}

			if (getDolGlobalString('TICKET_DISABLE_MAIL_AUTOCOPY_TO')) {
				$conf->global->MAIN_MAIL_AUTOCOPY_TO = $old_MAIN_MAIL_AUTOCOPY_TO;
			}
			//}
		} else {
			$langs->load("other");
			setEventMessages($langs->trans('ErrorMailRecipientIsEmptyForSendTicketMessage'), null, 'warnings');
		}

		return $is_sent;
	}

	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
	/**
	 *	Load indicators for dashboard (this->nbtodo and this->nbtodolate)
	 *
	 *  @param          User	$user   Object user
	 *  @param          'opened'|'signed'		$mode   "opened" for askprice to close, "signed" for proposal to invoice
	 *  @return         WorkboardResponse|int<-1,-1>	Return integer <0 if KO, WorkboardResponse if OK
	 */
	public function load_board($user, $mode)
	{
		// phpcs:enable
		global $user, $langs;

		$now = dol_now();
		$delay_warning = 0;

		$clause = " WHERE";

		$sql = "SELECT p.rowid, p.ref, p.datec as datec";
		$sql .= " FROM ".MAIN_DB_PREFIX."ticket as p";
		if (empty($user->socid) && isModEnabled('societe') && !$user->hasRight('societe', 'client', 'voir') && !$user->socid) {
			$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe_commerciaux as sc ON p.fk_soc = sc.fk_soc";
			$sql .= " WHERE sc.fk_user = ".((int) $user->id);
			$clause = " AND";
		}
		$sql .= $clause." p.entity IN (".getEntity('ticket').")";
		if ($mode == 'opened') {
			$sql .= " AND p.fk_statut NOT IN (".Ticket::STATUS_CLOSED.", ".Ticket::STATUS_CANCELED.")";
		}
		if ($user->socid) {
			$sql .= " AND p.fk_soc = ".((int) $user->socid);
		}

		$resql = $this->db->query($sql);
		if ($resql) {
			$label = $labelShort = '';
			$status = '';
			if ($mode == 'opened') {
				$status = 'openall';
				//$delay_warning = $conf->ticket->warning_delay;
				$delay_warning = 0;
				$label = $langs->trans("MenuListNonClosed");
				$labelShort = $langs->trans("MenuListNonClosed");
			}

			$response = new WorkboardResponse();
			//$response->warning_delay = $delay_warning / 60 / 60 / 24;
			$response->label = $label;
			$response->labelShort = $labelShort;
			$response->url = DOL_URL_ROOT.'/ticket/list.php?search_fk_statut[]='.$status;
			$response->img = img_object('', "ticket");

			// This assignment in condition is not a bug. It allows walking the results.
			while ($obj = $this->db->fetch_object($resql)) {
				$response->nbtodo++;
				if ($mode == 'opened') {
					$datelimit = (int) $this->db->jdate($obj->datec) + (int) $delay_warning;
					if ($datelimit < $now) {
						//$response->nbtodolate++;
					}
				}
			}
			return $response;
		} else {
			$this->error = $this->db->lasterror();
			return -1;
		}
	}

	/**
	 *      Load indicator this->nb of global stats widget
	 *
	 *      @return     int         Return integer <0 if ko, >0 if ok
	 */
	public function loadStateBoard()
	{
		global $user;

		$this->nb = array();
		$clause = "WHERE";

		$sql = "SELECT count(p.rowid) as nb";
		$sql .= " FROM ".MAIN_DB_PREFIX."ticket as p";
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON p.fk_soc = s.rowid";
		if (empty($user->socid) && !$user->hasRight('societe', 'client', 'voir')) {
			$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe_commerciaux as sc ON s.rowid = sc.fk_soc";
			$sql .= " WHERE sc.fk_user = ".((int) $user->id);
			$clause = "AND";
		}
		$sql .= " ".$clause." p.entity IN (".getEntity('ticket').")";

		$resql = $this->db->query($sql);
		if ($resql) {
			// This assignment in condition is not a bug. It allows walking the results.
			while ($obj = $this->db->fetch_object($resql)) {
				$this->nb["ticket"] = $obj->nb;
			}
			$this->db->free($resql);
			return 1;
		} else {
			dol_print_error($this->db);
			$this->error = $this->db->lasterror();
			return -1;
		}
	}

	/**
	 * Function used to replace a thirdparty id with another one.
	 *
	 * @param DoliDB 	$db 			Database handler
	 * @param int 		$origin_id 		Old thirdparty id
	 * @param int 		$dest_id 		New thirdparty id
	 * @return bool
	 */
	public static function replaceThirdparty($db, $origin_id, $dest_id)
	{
		$tables = array('ticket');

		return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
	}

	/**
	 *	Return clickable link of object (with eventually picto)
	 *
	 *	@param	string	    			$option		Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link)
	 *  @param	?array<string,mixed>	$arraydata	Array of data
	 *  @return	string								HTML Code for Kanban thumb.
	 */
	public function getKanbanView($option = '', $arraydata = null)
	{
		global $langs;

		$selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);

		$return = '<div class="box-flex-item box-flex-grow-zero">';
		$return .= '<div class="info-box info-box-sm">';
		$return .= '<span class="info-box-icon bg-infobox-action">';
		$return .= img_picto('', $this->picto);
		$return .= '</span>';
		$return .= '<div class="info-box-content">';
		$return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl(1) : $this->ref).'</span>';
		if ($selected >= 0) {
			$return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
		}
		if (!empty($arraydata['user_assignment'])) {
			$return .= '<br><span class="info-box-label" title="'.dol_escape_htmltag($langs->trans("AssignedTo")).'">'.$arraydata['user_assignment'].'</span>';
		}
		if (property_exists($this, 'type_code') && !empty($this->type_code)) {
			$return .= '<br>';
			$return .= '<div class="tdoverflowmax125 inline-block">'.$langs->getLabelFromKey($this->db, 'TicketTypeShort'.$this->type_code, 'c_ticket_type', 'code', 'label', $this->type_code).'</div>';
		}
		if (method_exists($this, 'getLibStatut')) {
			$return .= '<br><div class="info-box-status">'.$this->getLibStatut(3).'</div>';
		}
		$return .= '</div>';
		$return .= '</div>';
		$return .= '</div>';

		return $return;
	}

	/**
	 *  Create a document onto disk according to template module.
	 *
	 *  @param	string		$modele			Force template to use ('' to not force)
	 *  @param	Translate	$outputlangs	object lang a utiliser pour traduction
	 *  @param	int<0,1>	$hidedetails    Hide details of lines
	 *  @param	int<0,1>	$hidedesc       Hide description
	 *  @param	int<0,1>	$hideref        Hide ref
	 *  @param	?array<string,mixed>  $moreparams     Array to provide more information
	 *  @return	int         				0 if KO, 1 if OK
	 */
	public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
	{
		global $langs;

		$langs->load("ticket");
		$outputlangs->load("ticket");

		if (!dol_strlen($modele)) {
			$modele = 'generic_ticket_odt';

			if (!empty($this->model_pdf)) {
				$modele = $this->model_pdf;
			} elseif (getDolGlobalString('TICKET_ADDON_PDF')) {
				$modele = getDolGlobalString('TICKET_ADDON_PDF');
			}
		}

		$modelpath = "core/modules/ticket/doc/";

		return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
	}
}
